feat : #7 ajout du numero de telephone
All checks were successful
Build Check / build (push) Successful in 2m13s
Build Check / build (pull_request) Successful in 2m52s

This commit is contained in:
styve Lioumba
2025-12-23 13:10:49 +01:00
parent 0c269050a7
commit 73ac08e97a
14 changed files with 518 additions and 112 deletions

3
.gitignore vendored
View File

@@ -135,3 +135,6 @@ logs/
*.secrets
.act.secrets
.env
.vscode

View File

@@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
.vscode/launch.json vendored
View File

@@ -1,20 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored
View File

@@ -1,42 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "trouve-ton-profile",
"version": "1.3.1",
"version": "1.3.2",
"scripts": {
"ng": "ng",
"start": "bash replace-prod-env.sh src/environments/environment.development.ts $ENV_URL && ng serve",

View File

@@ -23,6 +23,8 @@ export interface ProfileViewModel {
settings?: SettingsProfileDto;
slug?: string;
userViewModel?: UserViewModel;
mail?: string;
phone?: string;
}
export interface ProfileViewModelPaginated {

View File

@@ -46,6 +46,7 @@ export class ProfilePresenter {
settings,
isProfileVisible,
missingFields,
phone: this.formatPhoneNumberForDisplay(profile.phone ? `${profile.phone.toString()}` : ''),
};
const profileExpand = (profile as any) ? (profile as any).expand : { utilisateur: {} as User };
@@ -58,7 +59,7 @@ export class ProfilePresenter {
const slug = userSlug === '' ? profileId : userSlug.concat('-', profileId);
const avatarUrl = userExpand.avatar
? `${environment.baseUrl}/api/files/users/${profile.utilisateur}/${userExpand.avatar}?thumb=320x240`
: `https://api.dicebear.com/9.x/initials/svg?seed=${userExpand.name}}`;
: `https://api.dicebear.com/9.x/initials/svg?seed=${userExpand.name}`;
profileViewModel = {
...profileViewModel,
@@ -66,6 +67,7 @@ export class ProfilePresenter {
slug,
fullName: userExpand.name,
avatarUrl,
mail: userExpand.email,
};
}
@@ -104,4 +106,31 @@ export class ProfilePresenter {
return missing;
}
private cleanPhoneNumber(phone: string): string {
if (!phone) return '';
return phone.replace(/\D/g, '');
}
private formatPhoneNumberForDisplay(phone: string): string {
if (!phone) return '';
const cleaned = this.cleanPhoneNumber(`0${phone}`);
// 9 chiffres : 06 xxx xx xx (3 + 2 + 2 + 2)
if (cleaned.length === 9) {
return `${cleaned.slice(0, 2)} ${cleaned.slice(2, 5)} ${cleaned.slice(5, 7)} ${cleaned.slice(7, 9)}`;
}
// 10 chiffres : 06 xx xx xx xx (5 groupes de 2)
if (cleaned.length === 10) {
const groups: string[] = [];
for (let i = 0; i < cleaned.length; i += 2) {
groups.push(cleaned.slice(i, i + 2));
}
return groups.join(' ');
}
return cleaned;
}
}

View File

@@ -16,6 +16,7 @@ export interface Profile {
estGeolocaliser?: boolean;
partageMail?: boolean;
partagePhone?: boolean;
phone?: number;
}
export interface ProfilePaginated {

View File

@@ -1,8 +1,8 @@
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { from, map, Observable } from 'rxjs';
import { from, Observable } from 'rxjs';
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
import { Injectable } from '@angular/core';
import PocketBase, { ListResult } from 'pocketbase';
import PocketBase from 'pocketbase';
import { environment } from '@env/environment';
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
import { SearchFilters } from '@app/domain/search/search-filters';

View File

@@ -42,6 +42,135 @@
<app-my-profile-update-cv-form [profile]="profile" />
</div>
<!-- Section mes coordonnées -->
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in animation-delay-100"
>
<div class="flex items-center gap-3 mb-6">
<div
class="w-10 h-10 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-purple-600 dark:text-purple-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
clip-rule="evenodd"
/>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Mes coordonnées</h3>
</div>
<div class="space-y-4">
<!-- Mail -->
<div>
<label for="mail" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Adresse email
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
class="w-5 h-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
</div>
<input
id="mail"
type="email"
formControlName="mail"
placeholder="mon-mail@domaine.extension"
readonly
[ngClass]="{
'border-red-500 focus:ring-red-500': isEmailInvalid,
'border-gray-300 dark:border-gray-600': !isEmailInvalid,
}"
class="w-full pl-10 pr-4 py-3 border rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-all"
/>
</div>
@if (isEmailInvalid) {
<p class="mt-1 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd"
/>
</svg>
{{ emailErrorMessage }}
</p>
}
</div>
<!-- Téléphone -->
<div>
<label
for="phone"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Numéro de téléphone
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
class="w-5 h-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z"
/>
</svg>
</div>
<input
id="phone"
type="text"
formControlName="phone"
placeholder="06 12 34 56 78"
maxlength="14"
[ngClass]="{
'border-red-500 focus:ring-red-500': isPhoneInvalid,
'border-gray-300 dark:border-gray-600': !isPhoneInvalid,
}"
class="w-full pl-10 pr-4 py-3 border rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-all"
/>
</div>
@if (isPhoneInvalid) {
<p class="mt-1 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd"
/>
</svg>
{{ phoneErrorMessage }}
</p>
}
</div>
</div>
</div>
<!-- Section À propos de moi -->
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in animation-delay-100"

View File

@@ -30,6 +30,8 @@ describe('MyProfileUpdateFormComponent', () => {
bio: '',
apropos: '',
reseaux: { facebook: '', github: '', instagram: '', linkedIn: '', web: '', x: '', youTube: '' },
phone: '123',
mail: 'test@mail.com',
};
beforeEach(async () => {
@@ -69,6 +71,8 @@ describe('MyProfileUpdateFormComponent', () => {
mockProfileData.secteur = 'technology';
mockProfileData.bio = 'A passionate developer';
mockProfileData.apropos = 'About me';
mockProfileData.mail = 'test@mail.com';
mockProfileData.phone = '123';
component.profileForm.setValue(mockProfileData);
const spyUpdateProfile = jest.spyOn(component, 'onSubmit');

View File

@@ -78,6 +78,11 @@ export class MyProfileUpdateFormComponent implements OnInit {
),
bio: new FormControl(this.profile.bio ? this.profile.bio.toLowerCase() : ''),
apropos: new FormControl(this.profile.apropos ? this.profile.apropos.toLowerCase() : ''),
mail: new FormControl(
{ value: this.profile.mail ? this.profile.mail.toLowerCase() : '', disabled: true },
[Validators.email]
),
phone: new FormControl(this.profile.phone ? this.profile.phone.toString().toLowerCase() : ''),
reseaux: new FormGroup({
facebook: new FormControl(
this.profile.reseaux ? (this.profile.reseaux as any)['facebook'] : ''
@@ -111,14 +116,36 @@ export class MyProfileUpdateFormComponent implements OnInit {
return;
}
const data: Profile = {
const data: Partial<Profile> = {
profession: this.profileForm.getRawValue().profession,
secteur: this.profileForm.getRawValue().secteur,
apropos: this.profileForm.getRawValue().apropos,
bio: this.profileForm.getRawValue().bio,
phone: parseInt(this.profileForm.getRawValue().phone.replace(/\D/g, '')),
reseaux: this.profileForm.getRawValue().reseaux,
} as Profile;
} as Partial<Profile>;
this.profileFacade.update(this.profile.id, data);
}
get isEmailInvalid(): boolean {
const mailControl = this.profileForm.get('mail');
return !!(mailControl?.invalid && mailControl?.touched && mailControl?.value);
}
get isPhoneInvalid(): boolean {
const phoneControl = this.profileForm.get('phone');
if (!phoneControl?.value) return false;
const phone = phoneControl.value.replace(/\D/g, '');
return phoneControl.touched && (phone.length < 9 || !phone.startsWith('0'));
}
get emailErrorMessage(): string {
return 'Veuillez entrer une adresse email valide';
}
get phoneErrorMessage(): string {
return 'Le numéro doit contenir minimum 9 chiffres (ex: 06 xxx xx xx)';
}
}

View File

@@ -1,56 +1,332 @@
import { ProfilePresenter } from '@app/adapters/profiles/profile.presenter';
import { Profile } from '@app/domain/profiles/profile.model';
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
import { mockProfiles } from '@app/testing/profile.mock';
import { Coordinates } from '@app/domain/localisation/coordinates.model';
import { UserViewModel } from '@app/adapters/users/user.presenter.model';
import { mockUsers } from '@app/testing/user.mock';
describe('ProfilePresenter', () => {
it('devrait transformer un Profile en ProfileViewModel', () => {
const profile: Profile = mockProfiles[0];
let presenter: ProfilePresenter;
const profilePresenter = new ProfilePresenter();
const viewModel = profilePresenter.toViewModel(profile);
const mockProfile: Profile = mockProfiles[0] as Profile;
expect(viewModel).toEqual({
id: profile.id,
fullName: '', // transformation OK
isVerifiedLabel: '✅ Vérifié',
estVerifier: true,
coordonnees: { latitude: 0, longitude: 0 } as Coordinates,
profession: 'Développeur Web'.toUpperCase(),
reseaux: { linkedin: 'https://linkedin.com/in/test' },
secteur: 'Informatique',
utilisateur: 'user_abc',
createdAtFormatted: new Date(profile.created).toLocaleDateString(),
avatarUrl: '',
apropos: 'Développeur Angular & Node.js',
bio: 'Passionné de code.',
cv: 'http://localhost:8090/api/files/profiles/1/cv.pdf',
isProfileVisible: true,
missingFields: [],
projets: ['p1', 'p2'],
settings: {
showEmail: false,
showPhone: false,
allowGeolocation: false,
isProfilePublic: false,
},
const mockUser: UserViewModel = mockUsers[0];
beforeEach(() => {
presenter = new ProfilePresenter();
});
describe('toViewModel', () => {
it('devrait convertir un profil en ProfileViewModel', () => {
const viewModel = presenter.toViewModel(mockProfile);
expect(viewModel).toBeDefined();
expect(viewModel.id).toBe('1');
expect(viewModel.profession).toBe('DÉVELOPPEUR WEB');
expect(viewModel.secteur).toBe('Informatique');
expect(viewModel.bio).toBe('Passionné de code.');
expect(viewModel.apropos).toBe('Développeur Angular & Node.js');
expect(viewModel.estVerifier).toBe(true);
expect(viewModel.isVerifiedLabel).toBe('✅ Vérifié');
});
it('devrait formater le numéro de téléphone à 10 chiffres', () => {
const profileWithPhone = {
...mockProfile,
phone: 612345678,
} as Profile;
const viewModel = presenter.toViewModel(profileWithPhone);
expect(viewModel.phone).toBe('06 12 34 56 78');
});
it('devrait formater le numéro de téléphone à 9 chiffres', () => {
const profileWithPhone = {
...mockProfile,
phone: 123456789,
} as Profile;
const viewModel = presenter.toViewModel(profileWithPhone);
expect(viewModel.phone).toBe('01 23 45 67 89');
});
it('devrait gérer un téléphone vide', () => {
const profileWithoutPhone = {
...mockProfile,
phone: undefined,
} as Profile;
const viewModel = presenter.toViewModel(profileWithoutPhone);
expect(viewModel.phone).toBe('');
});
it('devrait utiliser la bio par défaut si non fournie', () => {
const profileWithoutBio = {
...mockProfile,
bio: '',
} as Profile;
const viewModel = presenter.toViewModel(profileWithoutBio);
expect(viewModel.bio).toContain('Trouve Ton Profil');
expect(viewModel.bio).toContain('expertise');
});
it("devrait générer l'URL du CV correctement", () => {
const viewModel = presenter.toViewModel(mockProfile);
expect(viewModel.cv).toContain('/api/files/profiles/1/cv.pdf');
});
it('devrait retourner une chaîne vide si pas de CV', () => {
const profileWithoutCv = {
...mockProfile,
cv: '',
} as Profile;
const viewModel = presenter.toViewModel(profileWithoutCv);
expect(viewModel.cv).toBe('');
});
it('devrait définir les settings correctement', () => {
const viewModel = presenter.toViewModel(mockProfile);
expect(viewModel.settings).toBeDefined();
expect(viewModel.settings?.showEmail).toBe(false);
expect(viewModel.settings?.showPhone).toBe(false);
expect(viewModel.settings?.allowGeolocation).toBe(false);
expect(viewModel.settings?.isProfilePublic).toBe(false);
});
it('devrait définir les coordonnées correctement', () => {
const viewModel = presenter.toViewModel(mockProfile);
expect(viewModel.coordonnees).toBeDefined();
expect(viewModel.coordonnees?.latitude).toBe(0);
expect(viewModel.coordonnees?.longitude).toBe(0);
});
it("devrait gérer l'absence de coordonnées", () => {
const profileWithoutCoords = {
...mockProfile,
coordonnees: undefined,
} as Profile;
const viewModel = presenter.toViewModel(profileWithoutCoords);
expect(viewModel.coordonnees).toBeUndefined();
});
it('devrait formater la date de création', () => {
const viewModel = presenter.toViewModel(mockProfile);
expect(viewModel.createdAtFormatted).toBeDefined();
expect(typeof viewModel.createdAtFormatted).toBe('string');
});
it('devrait définir le label de vérification pour un profil non vérifié', () => {
const unverifiedProfile = {
...mockProfile,
estVerifier: false,
} as Profile;
const viewModel = presenter.toViewModel(unverifiedProfile);
expect(viewModel.isVerifiedLabel).toBe('❌ Non vérifié');
});
describe('avec données utilisateur (expand)', () => {
it('devrait inclure les informations utilisateur si disponibles', () => {
const profileWithExpand = {
...mockProfile,
expand: {
utilisateur: mockUser,
},
} as any;
const viewModel = presenter.toViewModel(profileWithExpand);
expect(viewModel.fullName).toBe('foo');
expect(viewModel.mail).toBe('foo@bar.com');
expect(viewModel.avatarUrl).toContain(
'http://localhost:8090/api/files/users/user_abc/foo.png?thumb=320x240'
);
expect(viewModel.userViewModel).toBeDefined();
});
it("devrait générer un slug à partir du username et de l'ID", () => {
const profileWithExpand = {
...mockProfile,
expand: {
utilisateur: {
...mockUser,
slug: 'test-user',
},
},
} as any;
const viewModel = presenter.toViewModel(profileWithExpand);
expect(viewModel.slug).toBe('foo-user001-1');
});
it("devrait utiliser l'ID du profil si pas de slug utilisateur", () => {
const profileWithExpand = {
...mockProfile,
expand: {
utilisateur: {
...mockUser,
slug: undefined,
},
},
} as any;
const viewModel = presenter.toViewModel(profileWithExpand);
expect(viewModel.slug).toBe('foo-user001-1');
});
it("devrait générer une URL Dicebear si pas d'avatar", () => {
const profileWithExpand = {
...mockProfile,
expand: {
utilisateur: {
...mockUser,
avatar: undefined,
},
},
} as any;
const viewModel = presenter.toViewModel(profileWithExpand);
expect(viewModel.avatarUrl).toContain('dicebear.com');
expect(viewModel.avatarUrl).toContain('foo');
});
});
});
it('devrait retourner ❌ Non vérifié si estVerifier = false', () => {
const profile = { ...mockProfiles[1], estVerifier: false };
describe('toViewModels', () => {
it('devrait convertir un tableau de profils', () => {
const profiles = [mockProfile, { ...mockProfile, id: '456' }];
const viewModels = presenter.toViewModels(profiles);
const profilePresenter = new ProfilePresenter();
const viewModel = profilePresenter.toViewModel(profile);
expect(viewModels).toHaveLength(2);
expect(viewModels[0].id).toBe('1');
expect(viewModels[1].id).toBe('456');
});
expect(viewModel.isVerifiedLabel).toBe('❌ Non vérifié');
it('devrait gérer un tableau vide', () => {
const viewModels = presenter.toViewModels([]);
expect(viewModels).toEqual([]);
});
});
it('devrait transformer un tableau complet', () => {
const profilePresenter = new ProfilePresenter();
const result = profilePresenter.toViewModels(mockProfiles);
describe('toViewModelPaginated', () => {
it('devrait convertir un ProfilePaginated en ProfileViewModelPaginated', () => {
const paginatedProfiles: ProfilePaginated = {
page: 1,
perPage: 10,
totalPages: 5,
totalItems: 50,
items: [mockProfile],
};
expect(result.length).toBe(2);
expect(result[0].fullName).toBe('');
const viewModel = presenter.toViewModelPaginated(paginatedProfiles);
expect(viewModel.page).toBe(1);
expect(viewModel.perPage).toBe(10);
expect(viewModel.totalPages).toBe(5);
expect(viewModel.totalItems).toBe(50);
expect(viewModel.items).toHaveLength(1);
expect(viewModel.items[0].id).toBe('1');
});
});
describe('isProfileVisible (méthode privée via toViewModel)', () => {
it('devrait marquer le profil comme visible si profession et secteur sont renseignés', () => {
const viewModel = presenter.toViewModel(mockProfile);
expect(viewModel.isProfileVisible).toBe(true);
expect(viewModel.missingFields).toEqual([]);
});
it("devrait marquer le profil comme non visible si la profession n'est pas renseignée", () => {
const profileWithoutProfession = {
...mockProfile,
profession: 'profession non renseignée',
} as Profile;
const viewModel = presenter.toViewModel(profileWithoutProfession);
expect(viewModel.isProfileVisible).toBe(false);
expect(viewModel.missingFields).toContain('profession');
});
it("devrait marquer le profil comme non visible si le secteur n'est pas renseigné", () => {
const profileWithoutSector = {
...mockProfile,
secteur: '',
} as Profile;
const viewModel = presenter.toViewModel(profileWithoutSector);
expect(viewModel.isProfileVisible).toBe(false);
expect(viewModel.missingFields).toContain("secteur d'activité");
});
it('devrait lister tous les champs manquants', () => {
const incompleteProfile = {
...mockProfile,
profession: 'profession non renseignée',
secteur: '',
} as Profile;
const viewModel = presenter.toViewModel(incompleteProfile);
expect(viewModel.isProfileVisible).toBe(false);
expect(viewModel.missingFields).toContain('profession');
expect(viewModel.missingFields).toContain("secteur d'activité");
});
});
describe('formatPhoneNumberForDisplay (méthode privée)', () => {
it('devrait formater un numéro à 10 chiffres', () => {
const profile = { ...mockProfile, phone: 612345678 } as Profile;
const viewModel = presenter.toViewModel(profile);
expect(viewModel.phone).toBe('06 12 34 56 78');
});
it('devrait formater un numéro à 9 chiffres', () => {
const profile = { ...mockProfile, phone: 123456789 } as Profile;
const viewModel = presenter.toViewModel(profile);
expect(viewModel.phone).toBe('01 23 45 67 89');
});
it('devrait retourner le numéro tel quel si moins de 9 chiffres', () => {
const profile = { ...mockProfile, phone: 1234567 } as Profile;
const viewModel = presenter.toViewModel(profile);
expect(viewModel.phone).toBe('01234567');
});
it('devrait retourner le numéro tel quel si plus de 10 chiffres', () => {
const profile = { ...mockProfile, phone: 12345678901 } as Profile;
const viewModel = presenter.toViewModel(profile);
expect(viewModel.phone).toBe('012345678901');
});
it('devrait retourner une chaîne vide pour un téléphone null', () => {
const profile = { ...mockProfile, phone: null } as any;
const viewModel = presenter.toViewModel(profile);
expect(viewModel.phone).toBe('');
});
});
});

View File

@@ -14,6 +14,7 @@ export const mockProfiles: Profile[] = [
reseaux: JSON.parse('{"linkedin": "https://linkedin.com/in/test"}'),
bio: 'Passionné de code.',
cv: 'cv.pdf',
phone: 123,
projets: ['p1', 'p2'],
apropos: 'Développeur Angular & Node.js',
},