Compare commits
3 Commits
6bab2e9ab2
...
625e2fea6c
| Author | SHA1 | Date | |
|---|---|---|---|
| 625e2fea6c | |||
| 3697c4b97c | |||
| bedb74ebd5 |
@@ -18,6 +18,7 @@ import { firstValueFrom } from 'rxjs';
|
|||||||
|
|
||||||
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
|
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
|
||||||
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
|
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
|
||||||
|
import { SimpleModalComponent } from '../../shared/components/simple-modal/simple-modal.component';
|
||||||
import {
|
import {
|
||||||
MODAL_DATA,
|
MODAL_DATA,
|
||||||
ModalRef,
|
ModalRef,
|
||||||
@@ -212,6 +213,41 @@ describe('ModalService', () => {
|
|||||||
showCloseButton: false
|
showCloseButton: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens the simple modal with default button label', () => {
|
||||||
|
service.openSimple({
|
||||||
|
title: 'Aviso',
|
||||||
|
content: 'Este es un aviso simple.'
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeModal = service.activeModal();
|
||||||
|
|
||||||
|
expect(activeModal?.component).toBe(SimpleModalComponent);
|
||||||
|
expect(activeModal?.config).toEqual({
|
||||||
|
title: 'Aviso',
|
||||||
|
data: {
|
||||||
|
content: 'Este es un aviso simple.',
|
||||||
|
buttonLabel: 'Entendido'
|
||||||
|
},
|
||||||
|
size: 'md',
|
||||||
|
closeOnBackdrop: true,
|
||||||
|
closeOnEscape: true,
|
||||||
|
showCloseButton: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps the simple modal close result to undefined', async () => {
|
||||||
|
const result$ = service.openSimple({
|
||||||
|
title: 'Aviso',
|
||||||
|
content: 'Este es un aviso simple.'
|
||||||
|
});
|
||||||
|
const activeModal = service.activeModal();
|
||||||
|
const resultPromise = firstValueFrom(result$);
|
||||||
|
|
||||||
|
activeModal?.ref.close();
|
||||||
|
|
||||||
|
await expect(resultPromise).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { InjectionToken, Injectable, Type, signal } from '@angular/core';
|
|||||||
import { Observable, Subject, map } from 'rxjs';
|
import { Observable, Subject, map } from 'rxjs';
|
||||||
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
|
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
|
||||||
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
|
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
|
||||||
|
import { SimpleModalComponent } from '../../shared/components/simple-modal/simple-modal.component';
|
||||||
|
|
||||||
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||||
export type ModalDismissReason =
|
export type ModalDismissReason =
|
||||||
@@ -43,6 +44,17 @@ export interface ConfirmModalConfig
|
|||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SimpleModalData {
|
||||||
|
content: string;
|
||||||
|
buttonLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SimpleModalConfig
|
||||||
|
extends Omit<ModalConfig<SimpleModalData>, 'data'> {
|
||||||
|
content: string;
|
||||||
|
buttonLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActiveModalState<TResult = unknown, TData = unknown> {
|
export interface ActiveModalState<TResult = unknown, TData = unknown> {
|
||||||
component: Type<unknown>;
|
component: Type<unknown>;
|
||||||
config: NormalizedModalConfig<TData>;
|
config: NormalizedModalConfig<TData>;
|
||||||
@@ -158,6 +170,19 @@ export class ModalService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openSimple(config: SimpleModalConfig): Observable<void> {
|
||||||
|
return this.openSimpleRef(config).afterClosed$.pipe(
|
||||||
|
map(() => undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
openSimpleRef(config: SimpleModalConfig): ModalRef<void> {
|
||||||
|
return this.open(
|
||||||
|
SimpleModalComponent,
|
||||||
|
this.buildSimpleModalConfig(config)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private close<TResult>(ref: ModalRef<TResult>, result?: TResult): void {
|
private close<TResult>(ref: ModalRef<TResult>, result?: TResult): void {
|
||||||
if (this.activeModalState()?.ref !== ref) {
|
if (this.activeModalState()?.ref !== ref) {
|
||||||
return;
|
return;
|
||||||
@@ -207,4 +232,22 @@ export class ModalService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildSimpleModalConfig(
|
||||||
|
config: SimpleModalConfig
|
||||||
|
): ModalConfig<SimpleModalData> {
|
||||||
|
const {
|
||||||
|
content,
|
||||||
|
buttonLabel = 'Entendido',
|
||||||
|
...modalConfig
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...modalConfig,
|
||||||
|
data: {
|
||||||
|
content,
|
||||||
|
buttonLabel
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,9 @@
|
|||||||
<app-button variant="danger-secondary" (click)="openWideModal()">
|
<app-button variant="danger-secondary" (click)="openWideModal()">
|
||||||
Abrir modal ancho
|
Abrir modal ancho
|
||||||
</app-button>
|
</app-button>
|
||||||
|
<app-button (click)="openSimpleModal()">
|
||||||
|
Abrir simple modal
|
||||||
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-showcase__result" data-testid="modal-last-result">
|
<div class="modal-showcase__result" data-testid="modal-last-result">
|
||||||
{{ lastModalResult }}
|
{{ lastModalResult }}
|
||||||
|
|||||||
@@ -226,6 +226,16 @@ export class ReutilizablesTestPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected openSimpleModal(): void {
|
||||||
|
this.modalService.openSimple({
|
||||||
|
title: 'Mensaje del sistema',
|
||||||
|
content: 'Este es un mensaje simple del sistema que no requiere confirmación.',
|
||||||
|
buttonLabel: 'Entendido'
|
||||||
|
}).subscribe(() => {
|
||||||
|
this.lastModalResult = 'Simple modal cerrado';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private openConfirmModal(config: Parameters<ModalService['openConfirm']>[0]): void {
|
private openConfirmModal(config: Parameters<ModalService['openConfirm']>[0]): void {
|
||||||
this.modalService.openConfirm(config).subscribe((confirmed) => {
|
this.modalService.openConfirm(config).subscribe((confirmed) => {
|
||||||
this.lastModalResult = `Resultado: ${confirmed}`;
|
this.lastModalResult = `Resultado: ${confirmed}`;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { provideRouter, Router } from '@angular/router';
|
|||||||
import { of, throwError } from 'rxjs';
|
import { of, throwError } from 'rxjs';
|
||||||
|
|
||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
|
import { ModalService } from '../../../../core/services/modal.service';
|
||||||
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import { RegisterPageComponent } from './register-page.component';
|
import { RegisterPageComponent } from './register-page.component';
|
||||||
|
|
||||||
describe('RegisterPageComponent', () => {
|
describe('RegisterPageComponent', () => {
|
||||||
@@ -23,10 +25,21 @@ describe('RegisterPageComponent', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
const modalService = {
|
||||||
|
openSimple: vi.fn().mockReturnValue(of(undefined))
|
||||||
|
};
|
||||||
|
const toastService = {
|
||||||
|
danger: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RegisterPageComponent],
|
imports: [RegisterPageComponent],
|
||||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ModalService, useValue: modalService },
|
||||||
|
{ provide: ToastService, useValue: toastService }
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||||
@@ -49,6 +62,10 @@ describe('RegisterPageComponent', () => {
|
|||||||
password: 'secret123',
|
password: 'secret123',
|
||||||
password_confirmation: 'secret123'
|
password_confirmation: 'secret123'
|
||||||
});
|
});
|
||||||
|
expect(modalService.openSimple).toHaveBeenCalledWith({
|
||||||
|
content: 'Tu cuenta fue creada correctamente',
|
||||||
|
buttonLabel: 'Cerrar'
|
||||||
|
});
|
||||||
expect(navigateSpy).toHaveBeenCalledWith(['/login']);
|
expect(navigateSpy).toHaveBeenCalledWith(['/login']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,10 +73,21 @@ describe('RegisterPageComponent', () => {
|
|||||||
const authService = {
|
const authService = {
|
||||||
register: vi.fn()
|
register: vi.fn()
|
||||||
};
|
};
|
||||||
|
const modalService = {
|
||||||
|
openSimple: vi.fn()
|
||||||
|
};
|
||||||
|
const toastService = {
|
||||||
|
danger: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RegisterPageComponent],
|
imports: [RegisterPageComponent],
|
||||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ModalService, useValue: modalService },
|
||||||
|
{ provide: ToastService, useValue: toastService }
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||||
@@ -81,10 +109,21 @@ describe('RegisterPageComponent', () => {
|
|||||||
const authService = {
|
const authService = {
|
||||||
register: vi.fn()
|
register: vi.fn()
|
||||||
};
|
};
|
||||||
|
const modalService = {
|
||||||
|
openSimple: vi.fn()
|
||||||
|
};
|
||||||
|
const toastService = {
|
||||||
|
danger: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RegisterPageComponent],
|
imports: [RegisterPageComponent],
|
||||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ModalService, useValue: modalService },
|
||||||
|
{ provide: ToastService, useValue: toastService }
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||||
@@ -116,10 +155,21 @@ describe('RegisterPageComponent', () => {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
const modalService = {
|
||||||
|
openSimple: vi.fn()
|
||||||
|
};
|
||||||
|
const toastService = {
|
||||||
|
danger: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RegisterPageComponent],
|
imports: [RegisterPageComponent],
|
||||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ModalService, useValue: modalService },
|
||||||
|
{ provide: ToastService, useValue: toastService }
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||||
@@ -134,5 +184,6 @@ describe('RegisterPageComponent', () => {
|
|||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
|
|
||||||
expect(component.serverError()).toBe('El email ya esta en uso.');
|
expect(component.serverError()).toBe('El email ya esta en uso.');
|
||||||
|
expect(toastService.danger).toHaveBeenCalledWith('El email ya esta en uso.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
|
import { ModalService } from '../../../../core/services/modal.service';
|
||||||
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||||
import { InputComponent } from '../../../../shared/components/input/input.component';
|
import { InputComponent } from '../../../../shared/components/input/input.component';
|
||||||
|
|
||||||
@@ -40,6 +42,8 @@ export class RegisterPageComponent {
|
|||||||
private readonly formBuilder = inject(FormBuilder);
|
private readonly formBuilder = inject(FormBuilder);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
|
private readonly modalService = inject(ModalService);
|
||||||
|
private readonly toastService = inject(ToastService);
|
||||||
|
|
||||||
private readonly submittedState = signal(false);
|
private readonly submittedState = signal(false);
|
||||||
private readonly serverErrorState = signal<string | null>(null);
|
private readonly serverErrorState = signal<string | null>(null);
|
||||||
@@ -78,11 +82,18 @@ export class RegisterPageComponent {
|
|||||||
this.authService.register(this.form.getRawValue()).subscribe({
|
this.authService.register(this.form.getRawValue()).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.isSubmittingState.set(false);
|
this.isSubmittingState.set(false);
|
||||||
void this.router.navigate(['/login']);
|
this.modalService.openSimple({
|
||||||
|
content: 'Tu cuenta fue creada correctamente',
|
||||||
|
buttonLabel: 'Cerrar'
|
||||||
|
}).subscribe(() => {
|
||||||
|
void this.router.navigate(['/login']);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (error: unknown) => {
|
error: (error: unknown) => {
|
||||||
this.isSubmittingState.set(false);
|
this.isSubmittingState.set(false);
|
||||||
this.serverErrorState.set(this.resolveErrorMessage(error));
|
const errorMessage = this.resolveErrorMessage(error);
|
||||||
|
this.serverErrorState.set(errorMessage);
|
||||||
|
this.toastService.danger(errorMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="d-grid gap-2 flex-grow-1 overflow-y-auto cart-items-container">
|
<div class="d-grid gap-2 flex-grow-1 overflow-y-auto cart-items-container">
|
||||||
@for (item of items(); track item.product + item.discountedPrice + resetKey(); let idx = $index) {
|
@for (item of items(); track item.productVariantId || item.product + item.discountedPrice; let idx = $index) {
|
||||||
<app-cart-item
|
<app-cart-item
|
||||||
[imageUrl]="item.imageUrl"
|
[imageUrl]="item.imageUrl"
|
||||||
[product]="item.product"
|
[product]="item.product"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
[discountedPrice]="item.discountedPrice"
|
[discountedPrice]="item.discountedPrice"
|
||||||
[discountPercentage]="item.discountPercentage"
|
[discountPercentage]="item.discountPercentage"
|
||||||
[attributes]="item.attributes"
|
[attributes]="item.attributes"
|
||||||
[quantity]="item.quantity"
|
[quantity]="getItemQuantity(item)"
|
||||||
(quantityChange)="onItemQuantityChange(idx, $event)"
|
(quantityChange)="onItemQuantityChange(idx, $event)"
|
||||||
(remove)="onItemRemove(idx)"
|
(remove)="onItemRemove(idx)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
BrowserTestingModule,
|
BrowserTestingModule,
|
||||||
platformBrowserTesting
|
platformBrowserTesting
|
||||||
} from '@angular/platform-browser/testing';
|
} from '@angular/platform-browser/testing';
|
||||||
import { of } from 'rxjs';
|
import { of, throwError } from 'rxjs';
|
||||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { CartService } from '../../../core/services/cart/cart.service';
|
import { CartService } from '../../../core/services/cart/cart.service';
|
||||||
@@ -141,4 +141,136 @@ describe('CartComponent', () => {
|
|||||||
expect(openConfirmDelete).toHaveBeenCalled();
|
expect(openConfirmDelete).toHaveBeenCalled();
|
||||||
expect(removeItem).not.toHaveBeenCalled();
|
expect(removeItem).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('optimistically updates quantity and rolls back on error', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const updateItemQuantity = vi.fn().mockReturnValue(throwError(() => new Error('Error')));
|
||||||
|
const danger = vi.fn();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CartComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CartService,
|
||||||
|
useValue: {
|
||||||
|
cart: signal(null).asReadonly(),
|
||||||
|
updateItemQuantity,
|
||||||
|
removeItem: vi.fn()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalService,
|
||||||
|
useValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ToastService,
|
||||||
|
useValue: {
|
||||||
|
success: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
danger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(CartComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
productVariantId: 10,
|
||||||
|
imageUrl: null,
|
||||||
|
product: 'Producto de prueba',
|
||||||
|
originalPrice: null,
|
||||||
|
discountedPrice: 1000,
|
||||||
|
discountPercentage: null,
|
||||||
|
attributes: [],
|
||||||
|
quantity: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
fixture.componentRef.setInput('items', [item]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Trigger quantity change to 3
|
||||||
|
fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('quantityChange', 3);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Optimistic update should be active immediately in local getter
|
||||||
|
expect((component as any).getItemQuantity(item)).toBe(3);
|
||||||
|
|
||||||
|
// Wait for the debounce time (1000ms)
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// After failure, it should roll back to original quantity (1)
|
||||||
|
expect((component as any).getItemQuantity(item)).toBe(1);
|
||||||
|
expect(danger).toHaveBeenCalled();
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('optimistically updates quantity and clears override on success', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const updateItemQuantity = vi.fn().mockReturnValue(of({ message: 'Success', data: {} }));
|
||||||
|
const success = vi.fn();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CartComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CartService,
|
||||||
|
useValue: {
|
||||||
|
cart: signal(null).asReadonly(),
|
||||||
|
updateItemQuantity,
|
||||||
|
removeItem: vi.fn()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalService,
|
||||||
|
useValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ToastService,
|
||||||
|
useValue: {
|
||||||
|
success,
|
||||||
|
info: vi.fn(),
|
||||||
|
danger: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(CartComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
productVariantId: 10,
|
||||||
|
imageUrl: null,
|
||||||
|
product: 'Producto de prueba',
|
||||||
|
originalPrice: null,
|
||||||
|
discountedPrice: 1000,
|
||||||
|
discountPercentage: null,
|
||||||
|
attributes: [],
|
||||||
|
quantity: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
fixture.componentRef.setInput('items', [item]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Trigger quantity change to 3
|
||||||
|
fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('quantityChange', 3);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Optimistic update should be active immediately in local getter
|
||||||
|
expect((component as any).getItemQuantity(item)).toBe(3);
|
||||||
|
|
||||||
|
// Wait for the debounce time (1000ms)
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// After success, it should clear override and use input quantity (which is 1 since we didn't update items input here)
|
||||||
|
expect((component as any).getItemQuantity(item)).toBe(1);
|
||||||
|
expect(success).toHaveBeenCalled();
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class CartComponent {
|
|||||||
|
|
||||||
readonly closed = output<void>();
|
readonly closed = output<void>();
|
||||||
|
|
||||||
protected readonly resetKey = signal(0);
|
protected readonly quantityOverrides = signal<Record<number, number>>({});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.quantityUpdates$.pipe(
|
this.quantityUpdates$.pipe(
|
||||||
@@ -55,12 +55,13 @@ export class CartComponent {
|
|||||||
next: (res) => {
|
next: (res) => {
|
||||||
const msg = res.message || 'Cantidad de producto actualizada.';
|
const msg = res.message || 'Cantidad de producto actualizada.';
|
||||||
this.toastService.success(msg);
|
this.toastService.success(msg);
|
||||||
|
this.clearOverride(update.productVariantId);
|
||||||
},
|
},
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
console.error('Error updating cart quantity', err);
|
console.error('Error updating cart quantity', err);
|
||||||
const msg = err.error?.message || 'Error al actualizar la cantidad del producto.';
|
const msg = err.error?.message || 'Error al actualizar la cantidad del producto.';
|
||||||
this.toastService.danger(msg);
|
this.toastService.danger(msg);
|
||||||
this.resetKey.update(k => k + 1);
|
this.clearOverride(update.productVariantId);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(() => EMPTY)
|
catchError(() => EMPTY)
|
||||||
@@ -70,10 +71,29 @@ export class CartComponent {
|
|||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getItemQuantity(item: CartItemMock): number {
|
||||||
|
if (item.productVariantId !== undefined && this.quantityOverrides()[item.productVariantId] !== undefined) {
|
||||||
|
return this.quantityOverrides()[item.productVariantId];
|
||||||
|
}
|
||||||
|
return item.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearOverride(productVariantId: number): void {
|
||||||
|
this.quantityOverrides.update((overrides) => {
|
||||||
|
const copy = { ...overrides };
|
||||||
|
delete copy[productVariantId];
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected onItemQuantityChange(index: number, newQuantity: number): void {
|
protected onItemQuantityChange(index: number, newQuantity: number): void {
|
||||||
const mockItem = this.items()[index];
|
const mockItem = this.items()[index];
|
||||||
const productVariantId = mockItem?.productVariantId;
|
const productVariantId = mockItem?.productVariantId;
|
||||||
if (productVariantId) {
|
if (productVariantId) {
|
||||||
|
this.quantityOverrides.update((overrides) => ({
|
||||||
|
...overrides,
|
||||||
|
[productVariantId]: newQuantity
|
||||||
|
}));
|
||||||
this.quantityUpdates$.next({
|
this.quantityUpdates$.next({
|
||||||
productVariantId,
|
productVariantId,
|
||||||
quantity: newQuantity
|
quantity: newQuantity
|
||||||
@@ -81,6 +101,10 @@ export class CartComponent {
|
|||||||
} else {
|
} else {
|
||||||
const item = this.cartService.cart()?.items[index];
|
const item = this.cartService.cart()?.items[index];
|
||||||
if (item) {
|
if (item) {
|
||||||
|
this.quantityOverrides.update((overrides) => ({
|
||||||
|
...overrides,
|
||||||
|
[item.product_variant_id]: newQuantity
|
||||||
|
}));
|
||||||
this.quantityUpdates$.next({
|
this.quantityUpdates$.next({
|
||||||
productVariantId: item.product_variant_id,
|
productVariantId: item.product_variant_id,
|
||||||
quantity: newQuantity
|
quantity: newQuantity
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-shell__content {
|
.modal-shell__content {
|
||||||
|
min-height: 180px;
|
||||||
max-height: calc(100dvh - 2rem);
|
max-height: calc(100dvh - 2rem);
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
background:
|
background:
|
||||||
@@ -72,6 +73,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-shell__body {
|
.modal-shell__body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
padding: 0 1.25rem 1.25rem;
|
padding: 0 1.25rem 1.25rem;
|
||||||
color: #303030;
|
color: #303030;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="simple-modal">
|
||||||
|
<p class="simple-modal__content">{{ data.content }}</p>
|
||||||
|
|
||||||
|
<div class="simple-modal__actions">
|
||||||
|
<app-button (click)="close()">
|
||||||
|
{{ data.buttonLabel }}
|
||||||
|
</app-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.simple-modal {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-modal__content {
|
||||||
|
margin: 0;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-modal__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import '@angular/compiler';
|
||||||
|
import { TestBed, getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting
|
||||||
|
} from '@angular/platform-browser/testing';
|
||||||
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SimpleModalData,
|
||||||
|
MODAL_DATA,
|
||||||
|
ModalRef
|
||||||
|
} from '../../../core/services/modal.service';
|
||||||
|
import { SimpleModalComponent } from './simple-modal.component';
|
||||||
|
|
||||||
|
describe('SimpleModalComponent', () => {
|
||||||
|
const data: SimpleModalData = {
|
||||||
|
content: 'Este es un mensaje simple.',
|
||||||
|
buttonLabel: 'Entendido'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
try {
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting()
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Test environment may already be initialized by another setup entrypoint.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the configured content and button label', async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SimpleModalComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MODAL_DATA,
|
||||||
|
useValue: data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalRef,
|
||||||
|
useValue: {
|
||||||
|
close: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(SimpleModalComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
|
expect(element.textContent).toContain(data.content);
|
||||||
|
expect(element.textContent).toContain(data.buttonLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes when clicking the primary button', async () => {
|
||||||
|
const closeSpy = vi.fn();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SimpleModalComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MODAL_DATA,
|
||||||
|
useValue: data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalRef,
|
||||||
|
useValue: {
|
||||||
|
close: closeSpy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(SimpleModalComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const button = fixture.nativeElement.querySelector('button');
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
expect(closeSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SimpleModalData,
|
||||||
|
MODAL_DATA,
|
||||||
|
ModalRef
|
||||||
|
} from '../../../core/services/modal.service';
|
||||||
|
import { ButtonComponent } from '../button/button.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-simple-modal',
|
||||||
|
imports: [ButtonComponent],
|
||||||
|
templateUrl: './simple-modal.component.html',
|
||||||
|
styleUrl: './simple-modal.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class SimpleModalComponent {
|
||||||
|
protected readonly data = inject<SimpleModalData>(MODAL_DATA);
|
||||||
|
private readonly modalRef = inject<ModalRef<void>>(ModalRef);
|
||||||
|
|
||||||
|
protected close(): void {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user