refactor(cart): remove cart showcase component and related functionality
This commit is contained in:
@@ -455,38 +455,6 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cart-showcase mt-4">
|
|
||||||
<div class="cart-showcase__header">
|
|
||||||
<div>
|
|
||||||
<h2>Cart copia</h2>
|
|
||||||
<p class="text-muted mb-0">
|
|
||||||
Copia aislada del carrito actual para iterar el rediseño sin tocar el componente productivo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!cartCopiaVisible) {
|
|
||||||
<app-button variant="secondary" (click)="showCartCopia()">
|
|
||||||
Volver a mostrar cart copia
|
|
||||||
</app-button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (cartCopiaVisible) {
|
|
||||||
<div class="cart-showcase__viewport">
|
|
||||||
<app-cart-copia
|
|
||||||
[showClose]="true"
|
|
||||||
[items]="cartMockItems"
|
|
||||||
[subtotal]="366000"
|
|
||||||
[discount]="66000"
|
|
||||||
[total]="300000"
|
|
||||||
[backgroundColor]="cartBackgroundColor"
|
|
||||||
(secondaryAction)="hideCartCopia()"
|
|
||||||
(primaryAction)="triggerCartCopiaCheckout()"
|
|
||||||
(closed)="hideCartCopia()"
|
|
||||||
></app-cart-copia>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -685,3 +653,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</app-store-section>
|
</app-store-section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -163,18 +163,6 @@ export class ReutilizablesTestPageComponent {
|
|||||||
this.toastService.info('Se disparo el evento de cerrado del carrito mock.');
|
this.toastService.info('Se disparo el evento de cerrado del carrito mock.');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected showCartCopia(): void {
|
|
||||||
this.cartCopiaVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected hideCartCopia(): void {
|
|
||||||
this.cartCopiaVisible = false;
|
|
||||||
this.toastService.info('Se disparo el evento de cerrado del cart-copia mock.');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected triggerCartCopiaCheckout(): void {
|
|
||||||
this.toastService.success('Se disparo la accion de compra del cart-copia mock.');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected triggerToast(type: 'success' | 'danger' | 'info'): void {
|
protected triggerToast(type: 'success' | 'danger' | 'info'): void {
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
@@ -273,3 +261,4 @@ export class ReutilizablesTestPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
<section class="cart-copia" [style.background-color]="backgroundColor()">
|
|
||||||
<header class="cart-copia__header">
|
|
||||||
<h2 class="cart-copia__title">{{ title() }}</h2>
|
|
||||||
|
|
||||||
@if (showClose()) {
|
|
||||||
<button
|
|
||||||
class="btn btn-link p-0 border-0 d-inline-flex align-items-center justify-content-center cart-copia__close-button"
|
|
||||||
type="button"
|
|
||||||
aria-label="Cerrar carrito"
|
|
||||||
(click)="closed.emit()"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-xmark"></i>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="cart-copia__body">
|
|
||||||
<div class="cart-copia__items">
|
|
||||||
@for (item of items(); track item.productVariantId || item.product + item.discountedPrice; let idx = $index) {
|
|
||||||
<article class="cart-copia__item">
|
|
||||||
<div class="cart-copia__media">
|
|
||||||
@if (item.discountPercentage && item.discountPercentage > 0) {
|
|
||||||
<span class="cart-copia__discount-badge">-{{ item.discountPercentage }}%</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (item.imageUrl) {
|
|
||||||
<img [src]="item.imageUrl" [alt]="item.product" />
|
|
||||||
} @else {
|
|
||||||
<div class="cart-copia__media-placeholder" aria-hidden="true"></div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-copia__content">
|
|
||||||
<div class="cart-copia__top-row">
|
|
||||||
<h3 class="cart-copia__product">{{ item.product }}</h3>
|
|
||||||
|
|
||||||
<div class="cart-copia__prices">
|
|
||||||
@if (item.originalPrice !== null) {
|
|
||||||
<span class="cart-copia__original-price">{{ formatCurrency(item.originalPrice) }}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
<span class="cart-copia__discounted-price">{{ formatCurrency(item.discountedPrice) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-copia__attributes">
|
|
||||||
@for (attribute of item.attributes; track attribute.label + attribute.value) {
|
|
||||||
<div class="cart-copia__attribute">
|
|
||||||
<span class="cart-copia__attribute-label">{{ attribute.label }}: </span>
|
|
||||||
<span class="cart-copia__attribute-value">{{ attribute.value }}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-copia__item-actions">
|
|
||||||
<div class="cart-copia__quantity-selector" aria-label="Selector de cantidad">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm p-0 border-0 d-inline-flex align-items-center justify-content-center cart-copia__quantity-button cart-copia__quantity-button--decrease"
|
|
||||||
type="button"
|
|
||||||
aria-label="Disminuir cantidad"
|
|
||||||
[disabled]="getItemQuantity(item) <= 1"
|
|
||||||
(click)="decreaseItemQuantity(idx)"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span class="cart-copia__quantity-value">{{ getItemQuantity(item) }}</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-sm p-0 border-0 d-inline-flex align-items-center justify-content-center cart-copia__quantity-button cart-copia__quantity-button--increase"
|
|
||||||
type="button"
|
|
||||||
aria-label="Aumentar cantidad"
|
|
||||||
(click)="increaseItemQuantity(idx)"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-link p-0 border-0 d-inline-flex align-items-center justify-content-center cart-copia__remove-button"
|
|
||||||
type="button"
|
|
||||||
aria-label="Eliminar producto"
|
|
||||||
(click)="onItemRemove(idx)"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="cart-copia__footer">
|
|
||||||
<div class="cart-copia__summary-row">
|
|
||||||
<span class="cart-copia__summary-label">Subtotal:</span>
|
|
||||||
<span class="cart-copia__summary-value">{{ formattedSubtotal() }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-copia__summary-row">
|
|
||||||
<span class="cart-copia__summary-label">Descuento:</span>
|
|
||||||
<span class="cart-copia__summary-value">{{ formattedDiscount() }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-copia__divider"></div>
|
|
||||||
|
|
||||||
<div class="cart-copia__total-row">
|
|
||||||
<span class="cart-copia__total-label">TOTAL:</span>
|
|
||||||
<span class="cart-copia__total-value">{{ formattedTotal() }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (showActions()) {
|
|
||||||
<div class="row g-2 mt-4 cart-copia__footer-actions">
|
|
||||||
<div class="col-12 col-sm-6">
|
|
||||||
<app-button
|
|
||||||
variant="secondary"
|
|
||||||
hostClass="w-100"
|
|
||||||
buttonClass="w-100 rounded-2 px-3 cart-copia__footer-app-button"
|
|
||||||
(click)="secondaryAction.emit()"
|
|
||||||
>
|
|
||||||
{{ secondaryActionLabel() }}
|
|
||||||
</app-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-sm-6">
|
|
||||||
<app-button
|
|
||||||
variant="primary"
|
|
||||||
hostClass="w-100"
|
|
||||||
buttonClass="w-100 rounded-2 px-3 cart-copia__footer-app-button"
|
|
||||||
(click)="primaryAction.emit()"
|
|
||||||
>
|
|
||||||
{{ primaryActionLabel() }}
|
|
||||||
</app-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</footer>
|
|
||||||
</section>
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
||||||
height: 100%;
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 18px 40px rgba(24, 30, 48, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 16px 16px 30px;
|
|
||||||
background: transparent;
|
|
||||||
border-bottom: 1px solid #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__title {
|
|
||||||
margin: 0;
|
|
||||||
color: #a6a6a6;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__close-button {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
color: #ababab;
|
|
||||||
font-size: 16px;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #7b7b7b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__body {
|
|
||||||
min-height: 0;
|
|
||||||
background: #f8f8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__items {
|
|
||||||
display: grid;
|
|
||||||
gap: 18px;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px 14px 20px 30px;
|
|
||||||
scrollbar-color: #d1d1d1 transparent;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #d1d1d1;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 88px minmax(0, 1fr);
|
|
||||||
gap: 14px;
|
|
||||||
padding-bottom: 18px;
|
|
||||||
border-bottom: 1px solid #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__media {
|
|
||||||
position: relative;
|
|
||||||
width: 88px;
|
|
||||||
height: 98px;
|
|
||||||
border-radius: 5px 0 0 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #d8d8d8;
|
|
||||||
|
|
||||||
img,
|
|
||||||
.cart-copia__media-placeholder {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__media-placeholder {
|
|
||||||
background: linear-gradient(180deg, #d9d9d9, #cecece);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__discount-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 4px 8px 3px;
|
|
||||||
background: var(--tenant-primary);
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__content {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__top-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__product {
|
|
||||||
margin: 0;
|
|
||||||
color: #7a7a7a;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.28;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__prices {
|
|
||||||
display: grid;
|
|
||||||
justify-items: end;
|
|
||||||
gap: 2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__original-price {
|
|
||||||
color: #b2b2b2;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 300;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__discounted-price {
|
|
||||||
color: #909090;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__attributes {
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__attribute {
|
|
||||||
color: #727272;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__attribute-label {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__attribute-value {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__item-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__quantity-selector {
|
|
||||||
display: inline-grid;
|
|
||||||
grid-template-columns: 16px 1fr 16px;
|
|
||||||
align-items: center;
|
|
||||||
width: 52px;
|
|
||||||
height: 23px;
|
|
||||||
border: 1px solid #cdcdcd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__quantity-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: transparent;
|
|
||||||
color: #6f6f6f;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
color: #c4c4c4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__quantity-value {
|
|
||||||
color: #666666;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
text-align: center;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__remove-button {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: #aaaaaa;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #7b7b7b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__footer {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 20px 30px 28px;
|
|
||||||
background: #ffffff;
|
|
||||||
border-top: 1px solid #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__summary-row,
|
|
||||||
.cart-copia__total-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__summary-label,
|
|
||||||
.cart-copia__summary-value {
|
|
||||||
color: #a9a9a9;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__summary-label {
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__summary-value {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__divider {
|
|
||||||
height: 1px;
|
|
||||||
margin: 6px 0 2px;
|
|
||||||
background: #efefef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__total-row {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__total-label,
|
|
||||||
.cart-copia__total-value {
|
|
||||||
color: #666666;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__footer-app-button {
|
|
||||||
min-height: 42px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
|
||||||
.cart-copia__header,
|
|
||||||
.cart-copia__footer {
|
|
||||||
padding-left: 18px;
|
|
||||||
padding-right: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__items {
|
|
||||||
padding-left: 18px;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__item {
|
|
||||||
grid-template-columns: 76px minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-copia__media {
|
|
||||||
width: 76px;
|
|
||||||
height: 88px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import '@angular/compiler';
|
|
||||||
import { signal } from '@angular/core';
|
|
||||||
import { TestBed, getTestBed } from '@angular/core/testing';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import {
|
|
||||||
BrowserTestingModule,
|
|
||||||
platformBrowserTesting
|
|
||||||
} from '@angular/platform-browser/testing';
|
|
||||||
import { of, throwError } from 'rxjs';
|
|
||||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { CartService } from '../../../core/services/cart/cart.service';
|
|
||||||
import { ModalService } from '../../../core/services/modal.service';
|
|
||||||
import { ToastService } from '../../../core/services/toast.service';
|
|
||||||
import { CartCopiaComponent } from './cart-copia.component';
|
|
||||||
|
|
||||||
describe('CartCopiaComponent', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
try {
|
|
||||||
getTestBed().initTestEnvironment(
|
|
||||||
BrowserTestingModule,
|
|
||||||
platformBrowserTesting()
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// Test environment may already be initialized by another setup entrypoint.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
TestBed.resetTestingModule();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('opens a confirm delete modal before removing an item', async () => {
|
|
||||||
const removeItem = vi.fn().mockReturnValue(of({ message: 'Producto eliminado.' }));
|
|
||||||
const openConfirmDelete = vi.fn().mockReturnValue(of(true));
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [CartCopiaComponent],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: CartService,
|
|
||||||
useValue: {
|
|
||||||
cart: signal(null).asReadonly(),
|
|
||||||
updateItemQuantity: vi.fn(),
|
|
||||||
removeItem
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ModalService,
|
|
||||||
useValue: {
|
|
||||||
openConfirmDelete
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ToastService,
|
|
||||||
useValue: {
|
|
||||||
success: vi.fn(),
|
|
||||||
info: vi.fn(),
|
|
||||||
danger: vi.fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(CartCopiaComponent);
|
|
||||||
fixture.componentRef.setInput('items', [
|
|
||||||
{
|
|
||||||
productVariantId: 10,
|
|
||||||
imageUrl: null,
|
|
||||||
product: 'Producto de prueba',
|
|
||||||
originalPrice: null,
|
|
||||||
discountedPrice: 1000,
|
|
||||||
discountPercentage: null,
|
|
||||||
attributes: [],
|
|
||||||
quantity: 1
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
fixture.debugElement.query(By.css('.cart-copia__remove-button')).triggerEventHandler('click');
|
|
||||||
|
|
||||||
expect(openConfirmDelete).toHaveBeenCalledWith({
|
|
||||||
title: 'Eliminar producto',
|
|
||||||
content:
|
|
||||||
'Se eliminara "Producto de prueba" del carrito. Esta accion no se puede deshacer.',
|
|
||||||
confirmLabel: 'Eliminar',
|
|
||||||
cancelLabel: 'Cancelar'
|
|
||||||
});
|
|
||||||
expect(removeItem).toHaveBeenCalledWith(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not remove the item when the delete confirmation is cancelled', async () => {
|
|
||||||
const removeItem = vi.fn().mockReturnValue(of({}));
|
|
||||||
const openConfirmDelete = vi.fn().mockReturnValue(of(false));
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [CartCopiaComponent],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: CartService,
|
|
||||||
useValue: {
|
|
||||||
cart: signal(null).asReadonly(),
|
|
||||||
updateItemQuantity: vi.fn(),
|
|
||||||
removeItem
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ModalService,
|
|
||||||
useValue: {
|
|
||||||
openConfirmDelete
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ToastService,
|
|
||||||
useValue: {
|
|
||||||
success: vi.fn(),
|
|
||||||
info: vi.fn(),
|
|
||||||
danger: vi.fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(CartCopiaComponent);
|
|
||||||
fixture.componentRef.setInput('items', [
|
|
||||||
{
|
|
||||||
productVariantId: 10,
|
|
||||||
imageUrl: null,
|
|
||||||
product: 'Producto de prueba',
|
|
||||||
originalPrice: null,
|
|
||||||
discountedPrice: 1000,
|
|
||||||
discountPercentage: null,
|
|
||||||
attributes: [],
|
|
||||||
quantity: 1
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
fixture.debugElement.query(By.css('.cart-copia__remove-button')).triggerEventHandler('click');
|
|
||||||
|
|
||||||
expect(openConfirmDelete).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: [CartCopiaComponent],
|
|
||||||
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(CartCopiaComponent);
|
|
||||||
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();
|
|
||||||
|
|
||||||
const increaseButton = fixture.debugElement.query(By.css('.cart-copia__quantity-button--increase'));
|
|
||||||
increaseButton.triggerEventHandler('click');
|
|
||||||
fixture.detectChanges();
|
|
||||||
increaseButton.triggerEventHandler('click');
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
expect((component as any).getItemQuantity(item)).toBe(3);
|
|
||||||
|
|
||||||
vi.advanceTimersByTime(1000);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
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: [CartCopiaComponent],
|
|
||||||
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(CartCopiaComponent);
|
|
||||||
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();
|
|
||||||
|
|
||||||
const increaseButton = fixture.debugElement.query(By.css('.cart-copia__quantity-button--increase'));
|
|
||||||
increaseButton.triggerEventHandler('click');
|
|
||||||
fixture.detectChanges();
|
|
||||||
increaseButton.triggerEventHandler('click');
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
expect((component as any).getItemQuantity(item)).toBe(3);
|
|
||||||
|
|
||||||
vi.advanceTimersByTime(1000);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
expect((component as any).getItemQuantity(item)).toBe(1);
|
|
||||||
expect(success).toHaveBeenCalled();
|
|
||||||
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal } from '@angular/core';
|
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import { Subject, EMPTY } from 'rxjs';
|
|
||||||
import { catchError, debounceTime, groupBy, mergeMap, switchMap, tap } from 'rxjs';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import type { CartItemAttribute } from '../cart-item/cart-item.component';
|
|
||||||
import { ButtonComponent } from '../button/button.component';
|
|
||||||
import { ModalService } from '../../../core/services/modal.service';
|
|
||||||
import { CartService } from '../../../core/services/cart/cart.service';
|
|
||||||
import { ToastService } from '../../../core/services/toast.service';
|
|
||||||
|
|
||||||
export interface CartCopiaItemMock {
|
|
||||||
productVariantId?: number;
|
|
||||||
imageUrl: string | null;
|
|
||||||
product: string;
|
|
||||||
originalPrice: number | null;
|
|
||||||
discountedPrice: number;
|
|
||||||
discountPercentage: number | null;
|
|
||||||
attributes: CartItemAttribute[];
|
|
||||||
quantity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-cart-copia',
|
|
||||||
standalone: true,
|
|
||||||
imports: [ButtonComponent],
|
|
||||||
templateUrl: './cart-copia.component.html',
|
|
||||||
styleUrl: './cart-copia.component.scss',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class CartCopiaComponent {
|
|
||||||
private readonly cartService = inject(CartService);
|
|
||||||
private readonly modalService = inject(ModalService);
|
|
||||||
private readonly toastService = inject(ToastService);
|
|
||||||
private readonly quantityUpdates$ = new Subject<{ productVariantId: number; quantity: number }>();
|
|
||||||
|
|
||||||
readonly title = input<string>('CARRITO');
|
|
||||||
readonly showClose = input<boolean>(false);
|
|
||||||
readonly items = input<CartCopiaItemMock[]>([]);
|
|
||||||
readonly subtotal = input<number>(0);
|
|
||||||
readonly discount = input<number>(0);
|
|
||||||
readonly total = input<number>(0);
|
|
||||||
readonly backgroundColor = input<string>('#ffffff');
|
|
||||||
readonly primaryActionLabel = input<string>('Comprar');
|
|
||||||
readonly secondaryActionLabel = input<string>('Seguir comprando');
|
|
||||||
readonly showActions = input<boolean>(true);
|
|
||||||
|
|
||||||
readonly closed = output<void>();
|
|
||||||
readonly primaryAction = output<void>();
|
|
||||||
readonly secondaryAction = output<void>();
|
|
||||||
|
|
||||||
protected readonly quantityOverrides = signal<Record<number, number>>({});
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.quantityUpdates$.pipe(
|
|
||||||
groupBy(update => update.productVariantId),
|
|
||||||
mergeMap(group$ => group$.pipe(
|
|
||||||
debounceTime(1000),
|
|
||||||
switchMap(update => this.cartService.updateItemQuantity(update.productVariantId, update.quantity).pipe(
|
|
||||||
tap({
|
|
||||||
next: (res) => {
|
|
||||||
const msg = res.message || 'Cantidad de producto actualizada.';
|
|
||||||
this.toastService.success(msg);
|
|
||||||
this.clearOverride(update.productVariantId);
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
console.error('Error updating cart quantity', err);
|
|
||||||
const msg = err.error?.message || 'Error al actualizar la cantidad del producto.';
|
|
||||||
this.toastService.danger(msg);
|
|
||||||
this.clearOverride(update.productVariantId);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
catchError(() => EMPTY)
|
|
||||||
))
|
|
||||||
)),
|
|
||||||
takeUntilDestroyed()
|
|
||||||
).subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getItemQuantity(item: CartCopiaItemMock): number {
|
|
||||||
if (item.productVariantId !== undefined && this.quantityOverrides()[item.productVariantId] !== undefined) {
|
|
||||||
return this.quantityOverrides()[item.productVariantId];
|
|
||||||
}
|
|
||||||
return item.quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected increaseItemQuantity(index: number): void {
|
|
||||||
const item = this.items()[index];
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onItemQuantityChange(index, this.getItemQuantity(item) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected decreaseItemQuantity(index: number): void {
|
|
||||||
const item = this.items()[index];
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextQuantity = Math.max(1, this.getItemQuantity(item) - 1);
|
|
||||||
|
|
||||||
if (nextQuantity !== this.getItemQuantity(item)) {
|
|
||||||
this.onItemQuantityChange(index, nextQuantity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearOverride(productVariantId: number): void {
|
|
||||||
this.quantityOverrides.update((overrides) => {
|
|
||||||
const copy = { ...overrides };
|
|
||||||
delete copy[productVariantId];
|
|
||||||
return copy;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onItemQuantityChange(index: number, newQuantity: number): void {
|
|
||||||
const mockItem = this.items()[index];
|
|
||||||
const productVariantId = mockItem?.productVariantId;
|
|
||||||
if (productVariantId) {
|
|
||||||
this.quantityOverrides.update((overrides) => ({
|
|
||||||
...overrides,
|
|
||||||
[productVariantId]: newQuantity
|
|
||||||
}));
|
|
||||||
this.quantityUpdates$.next({
|
|
||||||
productVariantId,
|
|
||||||
quantity: newQuantity
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const item = this.cartService.cart()?.items[index];
|
|
||||||
if (item) {
|
|
||||||
this.quantityOverrides.update((overrides) => ({
|
|
||||||
...overrides,
|
|
||||||
[item.product_variant_id]: newQuantity
|
|
||||||
}));
|
|
||||||
this.quantityUpdates$.next({
|
|
||||||
productVariantId: item.product_variant_id,
|
|
||||||
quantity: newQuantity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onItemRemove(index: number): void {
|
|
||||||
const target = this.resolveRemoveTarget(index);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.modalService.openConfirmDelete({
|
|
||||||
title: 'Eliminar producto',
|
|
||||||
content: `Se eliminara "${target.productName}" del carrito. Esta accion no se puede deshacer.`,
|
|
||||||
confirmLabel: 'Eliminar',
|
|
||||||
cancelLabel: 'Cancelar'
|
|
||||||
}).subscribe((confirmed) => {
|
|
||||||
if (confirmed) {
|
|
||||||
this.removeItem(target.productVariantId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly formattedSubtotal = computed(() => this.formatCurrency(this.subtotal()));
|
|
||||||
protected readonly formattedDiscount = computed(() => this.formatCurrency(this.discount()));
|
|
||||||
protected readonly formattedTotal = computed(() => this.formatCurrency(this.total()));
|
|
||||||
|
|
||||||
protected formatCurrency(value: number): string {
|
|
||||||
const rounded = Math.round(value);
|
|
||||||
const parts = rounded.toString().split('.');
|
|
||||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
||||||
return `$ ${parts.join(',')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveRemoveTarget(
|
|
||||||
index: number
|
|
||||||
): { productVariantId: number; productName: string } | null {
|
|
||||||
const mockItem = this.items()[index];
|
|
||||||
const productVariantId = mockItem?.productVariantId;
|
|
||||||
|
|
||||||
if (productVariantId) {
|
|
||||||
return {
|
|
||||||
productVariantId,
|
|
||||||
productName: mockItem.product
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = this.cartService.cart()?.items[index];
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
productVariantId: item.product_variant_id,
|
|
||||||
productName: item.product?.nombre ?? 'este producto'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeItem(productVariantId: number): void {
|
|
||||||
this.cartService.removeItem(productVariantId).subscribe({
|
|
||||||
next: (res) => {
|
|
||||||
const msg = res.message || 'Producto eliminado del carrito.';
|
|
||||||
this.toastService.info(msg);
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
console.error('Error removing item from cart', err);
|
|
||||||
const msg = err.error?.message || 'Error al eliminar el producto del carrito.';
|
|
||||||
this.toastService.danger(msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user