Compare commits
4 Commits
feature/al
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 520397baa5 | |||
| 85529f40f2 | |||
| 4a952af784 | |||
| b1d4c4d13d |
@@ -115,6 +115,6 @@ describe('App', () => {
|
|||||||
expect(fixture.nativeElement.textContent).toContain('No encontramos una tienda para este dominio');
|
expect(fixture.nativeElement.textContent).toContain('No encontramos una tienda para este dominio');
|
||||||
expect(document.title).toBe('ShopitFront');
|
expect(document.title).toBe('ShopitFront');
|
||||||
expect(document.head.querySelector<HTMLLinkElement>('link[rel~="icon"]')?.getAttribute('href'))
|
expect(document.head.querySelector<HTMLLinkElement>('link[rel~="icon"]')?.getAttribute('href'))
|
||||||
.toBe('favicon.ico');
|
.toBe("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { GlobalLoadingComponent } from './shared/components/global-loading/globa
|
|||||||
import { ModalHostComponent } from './shared/components/modal-host/modal-host.component';
|
import { ModalHostComponent } from './shared/components/modal-host/modal-host.component';
|
||||||
import { ToastContainerComponent } from './shared/components/toast-container/toast-container.component';
|
import { ToastContainerComponent } from './shared/components/toast-container/toast-container.component';
|
||||||
|
|
||||||
|
const EMPTY_FAVICON = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E";
|
||||||
|
|
||||||
function hexToRgb(hex: string): string {
|
function hexToRgb(hex: string): string {
|
||||||
const cleanHex = hex.replace('#', '').trim();
|
const cleanHex = hex.replace('#', '').trim();
|
||||||
let r = 0, g = 0, b = 0;
|
let r = 0, g = 0, b = 0;
|
||||||
@@ -66,7 +68,7 @@ export class App {
|
|||||||
effect(() => {
|
effect(() => {
|
||||||
const tenant = this.tenantService.tenant();
|
const tenant = this.tenantService.tenant();
|
||||||
const siteTitle = tenant?.site_title?.trim() || 'ShopitFront';
|
const siteTitle = tenant?.site_title?.trim() || 'ShopitFront';
|
||||||
const faviconHref = tenant?.favicon || 'favicon.ico';
|
const faviconHref = tenant?.favicon || EMPTY_FAVICON;
|
||||||
|
|
||||||
this.title.setTitle(siteTitle);
|
this.title.setTitle(siteTitle);
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,13 @@
|
|||||||
<label class="visually-hidden" for="register-password">Contraseña</label>
|
<label class="visually-hidden" for="register-password">Contraseña</label>
|
||||||
<app-input
|
<app-input
|
||||||
id="register-password"
|
id="register-password"
|
||||||
type="password"
|
type="password-toggle"
|
||||||
placeholder="Contraseña"
|
placeholder="Contraseña"
|
||||||
[value]="form.controls.password.value"
|
[value]="form.controls.password.value"
|
||||||
[invalid]="showControlError('password')"
|
[invalid]="showControlError('password')"
|
||||||
|
[visible]="passwordVisible()"
|
||||||
(valueChange)="updateField('password', $event)"
|
(valueChange)="updateField('password', $event)"
|
||||||
|
(visibleChange)="setPasswordVisibility($event)"
|
||||||
/>
|
/>
|
||||||
@if (getControlError('password'); as errorMessage) {
|
@if (getControlError('password'); as errorMessage) {
|
||||||
<small class="text-danger">{{ errorMessage }}</small>
|
<small class="text-danger">{{ errorMessage }}</small>
|
||||||
@@ -51,11 +53,13 @@
|
|||||||
<label class="visually-hidden" for="register-password-repeat">Repetir Contraseña</label>
|
<label class="visually-hidden" for="register-password-repeat">Repetir Contraseña</label>
|
||||||
<app-input
|
<app-input
|
||||||
id="register-password-repeat"
|
id="register-password-repeat"
|
||||||
type="password"
|
type="password-toggle"
|
||||||
placeholder="Repetir Contraseña"
|
placeholder="Repetir Contraseña"
|
||||||
[value]="form.controls.password_confirmation.value"
|
[value]="form.controls.password_confirmation.value"
|
||||||
[invalid]="showControlError('password_confirmation')"
|
[invalid]="showControlError('password_confirmation')"
|
||||||
|
[visible]="passwordVisible()"
|
||||||
(valueChange)="updateField('password_confirmation', $event)"
|
(valueChange)="updateField('password_confirmation', $event)"
|
||||||
|
(visibleChange)="setPasswordVisibility($event)"
|
||||||
/>
|
/>
|
||||||
@if (getControlError('password_confirmation'); as errorMessage) {
|
@if (getControlError('password_confirmation'); as errorMessage) {
|
||||||
<small class="text-danger">{{ errorMessage }}</small>
|
<small class="text-danger">{{ errorMessage }}</small>
|
||||||
|
|||||||
@@ -12,6 +12,48 @@ describe('RegisterPageComponent', () => {
|
|||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows and hides both password fields with either visibility control', async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [RegisterPageComponent],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AuthService, useValue: { register: vi.fn() } },
|
||||||
|
{ provide: ModalService, useValue: { openSimple: vi.fn() } },
|
||||||
|
{ provide: ToastService, useValue: { danger: vi.fn() } }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const getPasswordInputs = () =>
|
||||||
|
Array.from(
|
||||||
|
fixture.nativeElement.querySelectorAll(
|
||||||
|
'input#register-password, input#register-password-repeat'
|
||||||
|
)
|
||||||
|
) as HTMLInputElement[];
|
||||||
|
const getVisibilityButtons = () =>
|
||||||
|
Array.from(
|
||||||
|
fixture.nativeElement.querySelectorAll('button[aria-label]')
|
||||||
|
) as HTMLButtonElement[];
|
||||||
|
|
||||||
|
expect(getPasswordInputs().map((input) => input.type)).toEqual(['password', 'password']);
|
||||||
|
|
||||||
|
getVisibilityButtons()[0].click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getPasswordInputs().map((input) => input.type)).toEqual(['text', 'text']);
|
||||||
|
expect(getVisibilityButtons().map((button) => button.getAttribute('aria-label'))).toEqual([
|
||||||
|
'Ocultar contraseña',
|
||||||
|
'Ocultar contraseña'
|
||||||
|
]);
|
||||||
|
|
||||||
|
getVisibilityButtons()[1].click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getPasswordInputs().map((input) => input.type)).toEqual(['password', 'password']);
|
||||||
|
});
|
||||||
|
|
||||||
it('submits registration data and redirects to /login on success', async () => {
|
it('submits registration data and redirects to /login on success', async () => {
|
||||||
const authService = {
|
const authService = {
|
||||||
register: vi.fn().mockReturnValue(
|
register: vi.fn().mockReturnValue(
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export class RegisterPageComponent {
|
|||||||
private readonly submittedState = signal(false);
|
private readonly submittedState = signal(false);
|
||||||
private readonly serverErrorState = signal<string | null>(null);
|
private readonly serverErrorState = signal<string | null>(null);
|
||||||
private readonly isSubmittingState = signal(false);
|
private readonly isSubmittingState = signal(false);
|
||||||
|
private readonly passwordVisibleState = signal(false);
|
||||||
|
|
||||||
protected readonly form = this.formBuilder.nonNullable.group({
|
protected readonly form = this.formBuilder.nonNullable.group({
|
||||||
nombre_apellido: ['', [Validators.required, Validators.maxLength(TEXT_MAX_LENGTH)]],
|
nombre_apellido: ['', [Validators.required, Validators.maxLength(TEXT_MAX_LENGTH)]],
|
||||||
@@ -67,6 +68,11 @@ export class RegisterPageComponent {
|
|||||||
protected readonly submitted = this.submittedState.asReadonly();
|
protected readonly submitted = this.submittedState.asReadonly();
|
||||||
protected readonly serverError = this.serverErrorState.asReadonly();
|
protected readonly serverError = this.serverErrorState.asReadonly();
|
||||||
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
||||||
|
protected readonly passwordVisible = this.passwordVisibleState.asReadonly();
|
||||||
|
|
||||||
|
protected setPasswordVisibility(visible: boolean): void {
|
||||||
|
this.passwordVisibleState.set(visible);
|
||||||
|
}
|
||||||
|
|
||||||
goToLogin(): void {
|
goToLogin(): void {
|
||||||
void this.router.navigate(['/login']);
|
void this.router.navigate(['/login']);
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
<label class="visually-hidden" for="reset-password-new">Nueva Contraseña</label>
|
<label class="visually-hidden" for="reset-password-new">Nueva Contraseña</label>
|
||||||
<app-input
|
<app-input
|
||||||
id="reset-password-new"
|
id="reset-password-new"
|
||||||
type="password"
|
type="password-toggle"
|
||||||
placeholder="Nueva Contraseña"
|
placeholder="Nueva Contraseña"
|
||||||
[value]="form.controls.password.value"
|
[value]="form.controls.password.value"
|
||||||
[invalid]="showControlError('password')"
|
[invalid]="showControlError('password')"
|
||||||
|
[visible]="passwordVisible()"
|
||||||
(valueChange)="updatePassword('password', $event)"
|
(valueChange)="updatePassword('password', $event)"
|
||||||
|
(visibleChange)="setPasswordVisibility($event)"
|
||||||
/>
|
/>
|
||||||
@if (getControlError('password'); as errorMessage) {
|
@if (getControlError('password'); as errorMessage) {
|
||||||
<small class="text-danger">{{ errorMessage }}</small>
|
<small class="text-danger">{{ errorMessage }}</small>
|
||||||
@@ -30,11 +32,13 @@
|
|||||||
</label>
|
</label>
|
||||||
<app-input
|
<app-input
|
||||||
id="reset-password-confirmation"
|
id="reset-password-confirmation"
|
||||||
type="password"
|
type="password-toggle"
|
||||||
placeholder="Repetir Nueva Contraseña"
|
placeholder="Repetir Nueva Contraseña"
|
||||||
[value]="form.controls.password_confirmation.value"
|
[value]="form.controls.password_confirmation.value"
|
||||||
[invalid]="showControlError('password_confirmation')"
|
[invalid]="showControlError('password_confirmation')"
|
||||||
|
[visible]="passwordVisible()"
|
||||||
(valueChange)="updatePassword('password_confirmation', $event)"
|
(valueChange)="updatePassword('password_confirmation', $event)"
|
||||||
|
(visibleChange)="setPasswordVisibility($event)"
|
||||||
/>
|
/>
|
||||||
@if (getControlError('password_confirmation'); as errorMessage) {
|
@if (getControlError('password_confirmation'); as errorMessage) {
|
||||||
<small class="text-danger">{{ errorMessage }}</small>
|
<small class="text-danger">{{ errorMessage }}</small>
|
||||||
|
|||||||
@@ -50,6 +50,43 @@ describe('ResetPasswordPageComponent', () => {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('shows and hides both password fields with either visibility control', async () => {
|
||||||
|
const modalService = {
|
||||||
|
openSimple: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ResetPasswordPageComponent],
|
||||||
|
providers: resetProviders(modalService),
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ResetPasswordPageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const getPasswordInputs = () =>
|
||||||
|
Array.from(fixture.nativeElement.querySelectorAll('input')) as HTMLInputElement[];
|
||||||
|
const getVisibilityButtons = () =>
|
||||||
|
Array.from(
|
||||||
|
fixture.nativeElement.querySelectorAll('button[aria-label]'),
|
||||||
|
) as HTMLButtonElement[];
|
||||||
|
|
||||||
|
expect(getPasswordInputs().map((input) => input.type)).toEqual(['password', 'password']);
|
||||||
|
|
||||||
|
getVisibilityButtons()[0].click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getPasswordInputs().map((input) => input.type)).toEqual(['text', 'text']);
|
||||||
|
expect(getVisibilityButtons().map((button) => button.getAttribute('aria-label'))).toEqual([
|
||||||
|
'Ocultar contraseña',
|
||||||
|
'Ocultar contraseña',
|
||||||
|
]);
|
||||||
|
|
||||||
|
getVisibilityButtons()[1].click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getPasswordInputs().map((input) => input.type)).toEqual(['password', 'password']);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects passwords that do not match', async () => {
|
it('rejects passwords that do not match', async () => {
|
||||||
const modalService = {
|
const modalService = {
|
||||||
openSimple: vi.fn(),
|
openSimple: vi.fn(),
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class ResetPasswordPageComponent {
|
|||||||
private readonly submittedState = signal(false);
|
private readonly submittedState = signal(false);
|
||||||
private readonly isSubmittingState = signal(false);
|
private readonly isSubmittingState = signal(false);
|
||||||
private readonly serverErrorState = signal<string | null>(null);
|
private readonly serverErrorState = signal<string | null>(null);
|
||||||
|
private readonly passwordVisibleState = signal(false);
|
||||||
|
|
||||||
private readonly email = this.route.snapshot.queryParamMap.get('email') ?? '';
|
private readonly email = this.route.snapshot.queryParamMap.get('email') ?? '';
|
||||||
private readonly code = this.route.snapshot.queryParamMap.get('code') ?? '';
|
private readonly code = this.route.snapshot.queryParamMap.get('code') ?? '';
|
||||||
@@ -72,6 +73,11 @@ export class ResetPasswordPageComponent {
|
|||||||
protected readonly submitted = this.submittedState.asReadonly();
|
protected readonly submitted = this.submittedState.asReadonly();
|
||||||
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
||||||
protected readonly serverError = this.serverErrorState.asReadonly();
|
protected readonly serverError = this.serverErrorState.asReadonly();
|
||||||
|
protected readonly passwordVisible = this.passwordVisibleState.asReadonly();
|
||||||
|
|
||||||
|
protected setPasswordVisibility(visible: boolean): void {
|
||||||
|
this.passwordVisibleState.set(visible);
|
||||||
|
}
|
||||||
|
|
||||||
protected updatePassword(controlName: PasswordControlName, value: string | number): void {
|
protected updatePassword(controlName: PasswordControlName, value: string | number): void {
|
||||||
this.form.controls[controlName].setValue(String(value));
|
this.form.controls[controlName].setValue(String(value));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: true,
|
||||||
nombre:"Homologación - activo",
|
nombre:"Homologación - activo",
|
||||||
url:"https://backend.qa.shopit.com.ar/api/",
|
url:"https://backend.qa.shopit.com.ar/api/",
|
||||||
urlDescarga:"url/"
|
urlDescarga:"url/"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<title>ShopitFront</title>
|
<title>ShopitFront</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
Reference in New Issue
Block a user