Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
cc6818f
feat(frontend): setup add degree component dialog with dto #305
Aug 3, 2026
90c4c70
docs: improve documentation #305
Aug 4, 2026
801e4cf
feat: add new model #305
Aug 4, 2026
5c9fbff
test(frontend): setup add-degree component test #305
Aug 6, 2026
adaf1d4
test(frontend): testing addDegreeComponent #305
Aug 7, 2026
dafd0eb
test(frontend): tested ngOnInit in AddDegreeComponent #305
Aug 10, 2026
a9cbdb8
test(frontend): testing displayDegreeTypes #305
Aug 11, 2026
70f4afe
test(frontend): deleted non public methods test code #305
Aug 17, 2026
25ef507
style(frontend): deleted unnecessary spaces #305
Aug 17, 2026
28024f6
docs: rollback readme to main #305
Aug 17, 2026
397a0c9
test(frontend): fix test #305
Aug 18, 2026
3a2bd78
test(frontend): fix test #305
Aug 18, 2026
575fa42
feat(frontend): made new addExperience component #307
Aug 18, 2026
c2677de
refactor(frontend): renamed add-experience.component #307
Aug 18, 2026
689c722
feat(frontend): added new dialog experience #307
Aug 19, 2026
e8496c6
feat(frontend): add validation #307
Aug 19, 2026
3c9b2a7
feat(frontend): add and adjust translation #307
Aug 19, 2026
a6b2d1b
feat(frontend): add button #307
Aug 19, 2026
33a4139
feat(frontend): add openExperienceDialog #307
Aug 19, 2026
f3ff0b0
feat(frontend): renamed experienceType into type in html #307
Aug 24, 2026
9d52424
feat(frontend): made percent to null as number #307
Aug 24, 2026
0cfe4ff
revert(frontend): remove files from ticket 305 #307
Aug 24, 2026
b5e2dc3
feat(frontend): add modal actions #307
Aug 24, 2026
0476f1d
refactor(frontend): remove unnecessary import in add-experience-compo…
Aug 24, 2026
1c9bdcc
feat(frontend): add translation in validation for error message #307
Aug 25, 2026
0ceb560
feat(frontend): set limit for percent #307
Aug 25, 2026
f6c257a
test(frontend): add testing add-experience #307
Aug 25, 2026
0f05c46
feat(frontend): add limit in another form for percent #307
Aug 25, 2026
5ad63f4
fix(backend): remove annotaion for correct error message in degree an…
Aug 25, 2026
cea759d
refactor(frontend): remove scss file #307
Aug 26, 2026
a5e14c8
refactor(backend): remove annotation comment #307
Aug 26, 2026
ad882c1
feat(frontend): add translation for error message #307
Aug 26, 2026
f216586
feat: add validator for percent in add-experience.component #307
Aug 26, 2026
43f7a8d
refactor(frontend): remove completed checkbox #307
Aug 26, 2026
ffc0192
test(frontend): add test #307
Aug 26, 2026
bec04b1
feat(frontend): add new validator for percent #307
Aug 26, 2026
51a5561
test(frontend): add e2e test isInteger #307
Aug 31, 2026
13ed035
test(frontend): add unit test for isInteger validator #307
Aug 31, 2026
99a27cc
test(backend): add corrected test #307
Aug 31, 2026
f70ab64
fix(backend): remove unused test code #307
Sep 1, 2026
a38de50
fix(backend): remove unused test code #307
Sep 1, 2026
38b0f09
refactor(backend): remove comment in validationServiceTest #307
Sep 1, 2026
7bbab3a
refactor(frontend): refactor constructor in add experience component …
Sep 1, 2026
7e715f1
test(frontend): remove unit test #307
Sep 1, 2026
459d3b1
refactor(frontend): remove completed #307
Sep 2, 2026
a8104db
test(frontend): improve validation errors #307
Sep 2, 2026
bec460e
test(frontend): make isInteger call only one time #307
Sep 2, 2026
5be35c9
feat(frontend): new method checkModalIconButtonFocused #307
Sep 2, 2026
3448432
refactor(frontend): improve null handling in filter function #307
lcanobbio Sep 4, 2026
bcec3d9
test(frontend): fix tests #307
lcanobbio Sep 4, 2026
338e76a
fix(frontend): formatting #307
lcanobbio Sep 4, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import ch.puzzle.pcts.util.validation.PCTSStringValidation;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;
Expand Down Expand Up @@ -41,7 +40,6 @@ public class Degree implements Model {
private DegreeType degreeType;

@NotNull(message = "{attribute.not.null}")
@PastOrPresent(message = "{attribute.date.past.present}")
Comment thread
lcanobbio marked this conversation as resolved.
private LocalDate startDate;

private LocalDate endDate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;
Expand Down Expand Up @@ -47,7 +46,6 @@ public class Experience implements Model {
private String comment;

@NotNull(message = "{attribute.not.null}")
@PastOrPresent(message = "{attribute.date.past.present}")
Comment thread
lcanobbio marked this conversation as resolved.
private LocalDate startDate;

private LocalDate endDate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,7 @@ static Stream<Arguments> invalidModelProvider() {
List.of(Map.of(FieldKey.CLASS, "Degree", FieldKey.FIELD, "completed"))),
Arguments
.of(createDegree(MEMBER_1, "Computer Science", DEGREE_TYPE_1, true, null, DATE_NOW),
List.of(Map.of(FieldKey.CLASS, "Degree", FieldKey.FIELD, "startDate"))),
Arguments
.of(createDegree(MEMBER_1, "Computer Science", DEGREE_TYPE_1, true, DATE_TOMORROW, null),
List.of(Map.of(FieldKey.IS, "{attribute.date.past.present}"))));
List.of(Map.of(FieldKey.CLASS, "Degree", FieldKey.FIELD, "startDate"))));
}

@DisplayName("Should call correct validate method on validateOnCreate()")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,6 @@ static Stream<Arguments> invalidModelProvider() {
"2",
FieldKey.IS,
TOO_LONG_STRING))),
Arguments
.of(createExperience("Experience", DATE_TOMORROW, DATE_TOMORROW, validPercentage),
List.of(Map.of(FieldKey.IS, "{attribute.date.past.present}"))),
Arguments
.of(createExperience("Experience", null, DATE_NOW, validPercentage),
List.of(Map.of(FieldKey.CLASS, "Experience", FieldKey.FIELD, "startDate"))),
Expand Down
170 changes: 170 additions & 0 deletions frontend/cypress/e2e/add-experience.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import memberDetailPage from '../pages/memberDetailPage';
import modalPage from '../pages/modalPage';
import * as users from '../fixtures/users.json';
import formPage from '../pages/formPage';

describe('Add experience Modal', () => {
beforeEach(() => {
cy.loginAsUser(users.gl);
memberDetailPage.visit(1);
});

const openExperienceModal = () => {
memberDetailPage.openModalButton('add', 'experience')
.click();

modalPage.checkModalIconButtonVisible();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method not only checks if the button is visible but also whether the button is currently focused.
either rename the method or chanage the behavior as following:
the method should only return the icon button and the actual validation should be made in here
another idea would be to create a second method checkModalIconButtonFocused and then call both so the funtionality is instantly visible at the caller level

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this resolved?
The issue is still there?

modalPage.checkModalIconButtonFocused();
};

it('should open correct modal', () => {
openExperienceModal();

modalPage.modalTitle()
.should('include.text', 'Berufs- und Lebenserfahrung hinzufügen');
});

['ENTER_ANOTHER',
'COPY'].forEach((buttonType: string) => {
it(`should create experience via ${buttonType}`, () => {
cy.intercept('api/v1/experiences')
.as('experiences');

openExperienceModal();

formPage.submitButtonShouldBe('disabled');

modalPage.modalTitle()
.should('include.text', 'Berufs- und Lebenserfahrung hinzufügen');

modalPage.selectAutoCompleteValue('experienceType', 'Praktikum');

formPage.typeAndBlur('startDate', '10.10.2023');
formPage.typeAndBlur('endDate', '10.12.2027');
formPage.typeAndBlur('name', 'Software Engineer');
formPage.typeAndBlur('employer', 'TechNova Solutions');
formPage.typeAndBlur('percent', '100');
formPage.typeAndBlur('comment', 'Worked on backend APIs and DevOps tasks.');

formPage.submitButtonShouldBe('enabled');
formPage.clickSubmitMenuItem(buttonType);

formPage.shouldShowSuccessToast('Berufs- und Lebenserfahrung wurde erfolgreich erstellt.');

cy.get('@experiences')
.then((interception) => {
expect(interception.request.body).to.contain({
name: 'Software Engineer',
memberId: 1,
experienceTypeId: 1,
comment: 'Worked on backend APIs and DevOps tasks.',
percent: 100,
startDate: '2023-10-10',
endDate: '2027-12-10'
});
});

modalPage.checkModalIsClosed();
});
});

describe('Validation Errors', () => {
beforeEach(() => {
openExperienceModal();
});

it('validates experience type requirement and input', () => {
formPage.submitButtonShouldBe('disabled');

formPage.clearAndBlur('experienceType');

formPage.shouldShowValidationError('Muss ausgefüllt sein', 'experienceType');

formPage.typeAndBlur('experienceType', 'invalid entry');

formPage.shouldShowValidationError('Ungültige Eingabe', 'experienceType');
});

const fields = {
startDate: ['Muss ausgefüllt sein',
'Ungültiges Datum'],
endDate: ['Ungültiges Datum']
};

Object.entries(fields)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the benefit of using a a paramtrized test like this for 2 values?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benefit is that we don’t have to write the same test twice. Both fields are tested with the same logic. If we add another field later, we only need to add it to the list.

.forEach(([fieldName,
errors]) => {
it(`shows error for invalid date in: ${fieldName}`, () => {
formPage.submitButtonShouldBe('disabled');

formPage.typeAndBlur(fieldName, 'invalid entry');

errors.forEach((error) => {
formPage.shouldShowValidationError(error, fieldName);
});

formPage.submitButtonShouldBe('disabled');
});
});

it('validates percent requirement and integer input', () => {
formPage.submitButtonShouldBe('disabled');

formPage.typeAndBlur('percent', '0.9');

formPage.shouldShowValidationError('Die Eingabe muss eine Ganzzahl sein', 'percent');

formPage.submitButtonShouldBe('disabled');
});

it('validates percent maximum value', () => {
formPage.submitButtonShouldBe('disabled');

formPage.typeAndBlur('percent', '130');

formPage.shouldShowValidationError('Die Eingabe muss maximal 120 sein', 'percent');

formPage.submitButtonShouldBe('disabled');
});
});

describe('Error Toasts', () => {
beforeEach(() => {
openExperienceModal();

formPage.submitButtonShouldBe('disabled');
});

it('should show error when startDate is after endDate', () => {
modalPage.selectAutoCompleteValue('experienceType', 'Praktikum');

formPage.typeAndBlur('name', 'Software Engineer');
formPage.typeAndBlur('employer', 'TechNova Solutions');
formPage.typeAndBlur('percent', '100');
formPage.typeAndBlur('startDate', '10.10.2000');
formPage.typeAndBlur('endDate', '10.09.2000');

formPage.submitButtonShouldBe('enabled');

formPage.save();

formPage.shouldShowErrorToast('Von mit dem Wert 2000-10-10 muss jünger sein als 2000-09-10.');
});
});

describe('Closing Modal', () => {
beforeEach(() => {
openExperienceModal();
});

['icon-button',
'button'].forEach((buttonType: string) => {
it(`closes via ${buttonType}`, () => {
Comment thread
Thirusiga marked this conversation as resolved.
cy.getByTestId(`close-modal-${buttonType}`)
.click();

modalPage.checkModalIsClosed();
});
});
});
});
7 changes: 6 additions & 1 deletion frontend/cypress/pages/modalPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ModalPage extends Page {
}

selectAutoCompleteValue = (modelName: string, value: string) => {
formPage.type(`${modelName}`, modelName.slice(0, 3));
formPage.type(`${modelName}`, value.slice(0, 3));
cy.get('mat-option')
.contains(value)
.click();
Expand All @@ -27,6 +27,11 @@ class ModalPage extends Page {
.should('be.visible')
.and('be.focused');
}

checkModalIconButtonFocused() {
cy.getByTestId('close-modal-icon-button')
.should('be.focused');
}
}

export default new ModalPage();
16 changes: 13 additions & 3 deletions frontend/public/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
},
"EXPERIENCE": {
"MODEL_NAME": "Berufs- und Lebenserfahrung",
"COMMENT": "Beschreibung"
"COMMENT": "Beschreibung",
"NAME": "Bezeichnung",
"END_DATE": "Bis",
"EXPERIENCE_TYPE": "Art der Erfahrung",
"START_DATE": "Von",
"EMPLOYER": "Arbeitsort",
"PERCENT": "Stellenprozente",
"COMPLETED": "Abgeschlossen"
},
"CERTIFICATE": {
"MODEL_NAME": "Zertifikat",
Expand Down Expand Up @@ -74,12 +81,15 @@

"VALIDATION": {
"REQUIRED": "Muss ausgefüllt sein",
"DATE_IS_IN_FUTURE": "Datum muss in der Vergangenheit oder Gegenwart liegen.",
"DATE_IS_IN_FUTURE": "Datum muss in der Vergangenheit oder Gegenwart liegen",
"DATE_IS_NOT_IN_PAST": "Datum muss in der Vergangenheit liegen",
"INVALID_ENTRY": "Ungültige Eingabe",
"MATDATEPICKERPARSE": "Ungültiges Datum",
"NOT_AVAILABLE": "\"{{value}}\" nicht verfügbar",
"EMAIL": "Muss eine gültige Email-Adresse sein"
"EMAIL": "Muss eine gültige Email-Adresse sein",
"MIN": "Die Eingabe muss mindestens 0 sein",
"MAX": "Die Eingabe muss maximal 120 sein",
"INVALID_INTEGER": "Die Eingabe muss eine Ganzzahl sein"

Check warning on line 92 in frontend/public/i18n/de.json

View workflow job for this annotation

GitHub Actions / format / frontend

Unknown word: "Ganzzahl"
},

"MEMBER": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<app-base-modal [formGroup]="formGroup" (canceled)="onCancel()">
<app-base-form content [formGroup]="formGroup">
<ng-container title/>
<ng-container content>

<app-input-field>
<input type="text" formControlName="experienceType" matInput [matAutocomplete]="experienceTypeAutoComplete"
data-testid="experienceType">
<mat-autocomplete autoActiveFirstOption #experienceTypeAutoComplete="matAutocomplete"
[displayWith]="displayExperienceTypes">
@for (experienceType of experienceTypeFilteredOptions(); track experienceType) {
<mat-option [value]="experienceType">{{ displayExperienceTypes(experienceType) }}</mat-option>
}
</mat-autocomplete>
</app-input-field>

<app-input-field>
<input type="text" formControlName="name" data-testid="name" matInput>
</app-input-field>

<app-input-field>
<input type="text" formControlName="employer" data-testid="employer" matInput>
</app-input-field>

<app-input-field>
<input type="number" formControlName="percent" data-testid="percent" matInput>
</app-input-field>
Comment thread
Thirusiga marked this conversation as resolved.

<mat-form-field>
<mat-label appPctsFormLabel/>
<input matInput [matDatepicker]="completedAtPicker" formControlName="startDate" data-testid="startDate">
<mat-datepicker-toggle matIconSuffix [for]="completedAtPicker"/>
<mat-datepicker #completedAtPicker/>
<mat-error appPctsFormError/>
</mat-form-field>

<mat-form-field>
<mat-label appPctsFormLabel/>
<input matInput [matDatepicker]="validUntilPicker" formControlName="endDate" data-testid="endDate">
<mat-datepicker-toggle matIconSuffix [for]="validUntilPicker"/>
<mat-datepicker #validUntilPicker/>
<mat-error appPctsFormError/>
</mat-form-field>

<app-input-field>
<textarea type="text" formControlName="comment" data-testid="comment" matInput></textarea>
</app-input-field>

</ng-container>
<ng-container actions/>
</app-base-form>

<ng-container actions>
<app-modal-actions
[isValid]="formGroup.valid"
(cancelAction)="onCancel()"
(submitAction)="onSubmit($event)" />
</ng-container>
</app-base-modal>
Loading
Loading