feat(catalog): refresh availability after cart changes
This commit is contained in:
@@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
|
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { Tenant } from '../../../../core/services/tenant.interface';
|
import { Tenant } from '../../../../core/services/tenant.interface';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
@@ -107,6 +108,16 @@ describe('CategoryItemsPageComponent', () => {
|
|||||||
expect(Array.isArray(productList.items())).toBe(false);
|
expect(Array.isArray(productList.items())).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reloads the current category page when catalog availability changes', () => {
|
||||||
|
const fixture = TestBed.createComponent(CategoryItemsPageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
TestBed.inject(CatalogAvailabilityService).notifyAvailabilityChanged();
|
||||||
|
|
||||||
|
expect(getCategoryItems).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getCategoryItems).toHaveBeenLastCalledWith(7, { page: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
it('does not request the API when the category id is invalid', () => {
|
it('does not request the API when the category id is invalid', () => {
|
||||||
TestBed.overrideProvider(ActivatedRoute, {
|
TestBed.overrideProvider(ActivatedRoute, {
|
||||||
useValue: {
|
useValue: {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
finalize,
|
finalize,
|
||||||
map,
|
map,
|
||||||
of,
|
of,
|
||||||
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
CategoryItemsResponse,
|
CategoryItemsResponse,
|
||||||
} from '../../../../core/services/catalog/catalog.interface';
|
} from '../../../../core/services/catalog/catalog.interface';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +52,7 @@ interface CategoryRouteState {
|
|||||||
})
|
})
|
||||||
export class CategoryItemsPageComponent {
|
export class CategoryItemsPageComponent {
|
||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
|
private readonly catalogAvailabilityService = inject(CatalogAvailabilityService);
|
||||||
private readonly catalogService = inject(CatalogService);
|
private readonly catalogService = inject(CatalogService);
|
||||||
private readonly injector = inject(Injector);
|
private readonly injector = inject(Injector);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
@@ -65,22 +68,29 @@ export class CategoryItemsPageComponent {
|
|||||||
protected readonly paginatedLayout: CatalogGroupLayout = 'paginated';
|
protected readonly paginatedLayout: CatalogGroupLayout = 'paginated';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
combineLatest([this.route.paramMap, this.route.queryParamMap])
|
const routeState$ = combineLatest([this.route.paramMap, this.route.queryParamMap]).pipe(
|
||||||
.pipe(
|
map(
|
||||||
map(
|
([params, queryParams]): CategoryRouteState => ({
|
||||||
([params, queryParams]): CategoryRouteState => ({
|
categoryId: this.parsePositiveInteger(params.get('id')),
|
||||||
categoryId: this.parsePositiveInteger(params.get('id')),
|
page: this.parsePositiveInteger(queryParams.get('page'), 1),
|
||||||
page: this.parsePositiveInteger(queryParams.get('page'), 1),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
distinctUntilChanged(
|
|
||||||
(previous, current) =>
|
|
||||||
previous.categoryId === current.categoryId && previous.page === current.page,
|
|
||||||
),
|
|
||||||
tap(() => {
|
|
||||||
this.results.set(null);
|
|
||||||
this.error.set(null);
|
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
distinctUntilChanged(
|
||||||
|
(previous, current) =>
|
||||||
|
previous.categoryId === current.categoryId && previous.page === current.page,
|
||||||
|
),
|
||||||
|
tap(() => {
|
||||||
|
this.results.set(null);
|
||||||
|
this.error.set(null);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
routeState$,
|
||||||
|
this.catalogAvailabilityService.availabilityChanged$.pipe(startWith(undefined)),
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([routeState]) => routeState),
|
||||||
switchMap(({ categoryId, page }) => {
|
switchMap(({ categoryId, page }) => {
|
||||||
if (categoryId === 0) {
|
if (categoryId === 0) {
|
||||||
this.error.set('La categoría solicitada no es válida.');
|
this.error.set('La categoría solicitada no es válida.');
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
|
|
||||||
import { CatalogItemDetail } from '../../../../core/services/catalog/catalog.interface';
|
import { CatalogItemDetail } from '../../../../core/services/catalog/catalog.interface';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import { ProductDetailPageComponent } from './product-detail-page.component';
|
import { ProductDetailPageComponent } from './product-detail-page.component';
|
||||||
@@ -176,6 +177,21 @@ describe('ProductDetailPageComponent', () => {
|
|||||||
expect(element.querySelector('.product-carousel__discount-badge')).toBeNull();
|
expect(element.querySelector('.product-carousel__discount-badge')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reloads product availability when the cart changes', async () => {
|
||||||
|
catalogServiceStub.getCatalogItem.mockReturnValue(
|
||||||
|
of({ ...mockProduct, maximum_addable_quantity: 4 }),
|
||||||
|
);
|
||||||
|
await configureTestingModule();
|
||||||
|
const fixture = TestBed.createComponent(ProductDetailPageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
TestBed.inject(CatalogAvailabilityService).notifyAvailabilityChanged();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(catalogServiceStub.getCatalogItem).toHaveBeenCalledWith(1, undefined);
|
||||||
|
expect(fixture.componentInstance['selectedVariantMax']()).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows error message if the resolver cannot load the product', async () => {
|
it('shows error message if the resolver cannot load the product', async () => {
|
||||||
resolveProductError(PRODUCT_DETAIL_ERROR_MESSAGE);
|
resolveProductError(PRODUCT_DETAIL_ERROR_MESSAGE);
|
||||||
await configureTestingModule();
|
await configureTestingModule();
|
||||||
@@ -510,9 +526,9 @@ describe('ProductDetailPageComponent', () => {
|
|||||||
await configureTestingModule();
|
await configureTestingModule();
|
||||||
const fixture = TestBed.createComponent(ProductDetailPageComponent);
|
const fixture = TestBed.createComponent(ProductDetailPageComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const buyButton = Array.from(fixture.nativeElement.querySelectorAll('app-button button')).find(
|
const buyButton = Array.from(
|
||||||
(button) => button.textContent?.trim() === 'Comprar',
|
fixture.nativeElement.querySelectorAll('app-button button') as NodeListOf<HTMLButtonElement>,
|
||||||
) as HTMLButtonElement;
|
).find((button) => button.textContent?.trim() === 'Comprar') as HTMLButtonElement;
|
||||||
|
|
||||||
buyButton.click();
|
buyButton.click();
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import {
|
import {
|
||||||
@@ -51,6 +52,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly catalogService = inject(CatalogService);
|
private readonly catalogService = inject(CatalogService);
|
||||||
|
private readonly catalogAvailabilityService = inject(CatalogAvailabilityService);
|
||||||
private readonly toastService = inject(ToastService);
|
private readonly toastService = inject(ToastService);
|
||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
private readonly checkoutService = inject(CheckoutService);
|
private readonly checkoutService = inject(CheckoutService);
|
||||||
@@ -64,6 +66,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private routeSub: Subscription | null = null;
|
private routeSub: Subscription | null = null;
|
||||||
private productSub: Subscription | null = null;
|
private productSub: Subscription | null = null;
|
||||||
|
private availabilityChangedSub: Subscription | null = null;
|
||||||
private carouselResizeObserver: ResizeObserver | null = null;
|
private carouselResizeObserver: ResizeObserver | null = null;
|
||||||
private observedCarouselPreview: HTMLElement | null = null;
|
private observedCarouselPreview: HTMLElement | null = null;
|
||||||
private measurementTimer: ReturnType<typeof setTimeout> | null = null;
|
private measurementTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -150,6 +153,10 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.availabilityChangedSub = this.catalogAvailabilityService.availabilityChanged$.subscribe(
|
||||||
|
() => this.refreshProductAvailability(),
|
||||||
|
);
|
||||||
|
|
||||||
this.routeSub = this.route.data.subscribe((data) => {
|
this.routeSub = this.route.data.subscribe((data) => {
|
||||||
const resolvedData = data['productDetailData'] as ProductDetailResolvedData | undefined;
|
const resolvedData = data['productDetailData'] as ProductDetailResolvedData | undefined;
|
||||||
|
|
||||||
@@ -160,6 +167,7 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.availabilityChangedSub?.unsubscribe();
|
||||||
this.routeSub?.unsubscribe();
|
this.routeSub?.unsubscribe();
|
||||||
this.productSub?.unsubscribe();
|
this.productSub?.unsubscribe();
|
||||||
this.carouselResizeObserver?.disconnect();
|
this.carouselResizeObserver?.disconnect();
|
||||||
@@ -196,6 +204,32 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private refreshProductAvailability(): void {
|
||||||
|
const currentProduct = this.product();
|
||||||
|
if (!currentProduct) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantId = this.selectedVariant()?.id;
|
||||||
|
this.productSub?.unsubscribe();
|
||||||
|
this.productSub = this.catalogService
|
||||||
|
.withCustomLoading()
|
||||||
|
.getCatalogItem(currentProduct.id, variantId)
|
||||||
|
.subscribe({
|
||||||
|
next: (product) => {
|
||||||
|
this.applyProduct(product, false);
|
||||||
|
|
||||||
|
const maximum = this.selectedVariantMax();
|
||||||
|
if (maximum !== null && this.quantity() > maximum) {
|
||||||
|
this.quantity.set(Math.max(1, maximum));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
// Keep the last known availability if the silent refresh fails.
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private applyResolvedData(resolvedData: ProductDetailResolvedData): void {
|
private applyResolvedData(resolvedData: ProductDetailResolvedData): void {
|
||||||
this.productSub?.unsubscribe();
|
this.productSub?.unsubscribe();
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
describe('productDetailResolver', () => {
|
describe('productDetailResolver', () => {
|
||||||
const product: CatalogItemDetail = {
|
const product: CatalogItemDetail = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
type: 'product',
|
||||||
category_id: 10,
|
category_id: 10,
|
||||||
brand_id: null,
|
brand_id: null,
|
||||||
slug: 'auriculares-bluetooth',
|
slug: 'auriculares-bluetooth',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
|
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { Tenant } from '../../../../core/services/tenant.interface';
|
import { Tenant } from '../../../../core/services/tenant.interface';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
@@ -108,6 +109,16 @@ describe('SearchPageComponent', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('repeats the current search when catalog availability changes', () => {
|
||||||
|
const fixture = TestBed.createComponent(SearchPageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
TestBed.inject(CatalogAvailabilityService).notifyAvailabilityChanged();
|
||||||
|
|
||||||
|
expect(searchCatalog).toHaveBeenCalledTimes(2);
|
||||||
|
expect(searchCatalog).toHaveBeenLastCalledWith({ q: 'running', page: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the search title and query subtitle with the category header layout', () => {
|
it('renders the search title and query subtitle with the category header layout', () => {
|
||||||
const fixture = TestBed.createComponent(SearchPageComponent);
|
const fixture = TestBed.createComponent(SearchPageComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|||||||
@@ -9,7 +9,17 @@ import {
|
|||||||
signal,
|
signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { catchError, distinctUntilChanged, finalize, map, of, switchMap, tap } from 'rxjs';
|
import {
|
||||||
|
catchError,
|
||||||
|
combineLatest,
|
||||||
|
distinctUntilChanged,
|
||||||
|
finalize,
|
||||||
|
map,
|
||||||
|
of,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
@@ -22,6 +32,7 @@ import {
|
|||||||
CatalogProductLayout,
|
CatalogProductLayout,
|
||||||
} from '../../../../core/services/catalog/catalog.interface';
|
} from '../../../../core/services/catalog/catalog.interface';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { ApiPaginatedResponse } from '../../../../core/services/api-paginated-response.interface';
|
import { ApiPaginatedResponse } from '../../../../core/services/api-paginated-response.interface';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
@@ -46,6 +57,7 @@ interface SearchRouteState {
|
|||||||
export class SearchPageComponent {
|
export class SearchPageComponent {
|
||||||
private readonly minSearchLength = 3;
|
private readonly minSearchLength = 3;
|
||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
|
private readonly catalogAvailabilityService = inject(CatalogAvailabilityService);
|
||||||
private readonly catalogService = inject(CatalogService);
|
private readonly catalogService = inject(CatalogService);
|
||||||
private readonly injector = inject(Injector);
|
private readonly injector = inject(Injector);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
@@ -86,26 +98,33 @@ export class SearchPageComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.route.queryParamMap
|
const routeState$ = this.route.queryParamMap.pipe(
|
||||||
.pipe(
|
map(
|
||||||
map(
|
(params): SearchRouteState => ({
|
||||||
(params): SearchRouteState => ({
|
query: params.get('q')?.trim() ?? '',
|
||||||
query: params.get('q')?.trim() ?? '',
|
page: this.parsePage(params.get('page')),
|
||||||
page: this.parsePage(params.get('page')),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
distinctUntilChanged(
|
|
||||||
(previous, current) => previous.query === current.query && previous.page === current.page,
|
|
||||||
),
|
|
||||||
tap(({ query }) => {
|
|
||||||
this.query.set(query);
|
|
||||||
this.results.set(null);
|
|
||||||
this.error.set(
|
|
||||||
query.length >= this.minSearchLength
|
|
||||||
? null
|
|
||||||
: `Ingresá al menos ${this.minSearchLength} caracteres para buscar.`,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
distinctUntilChanged(
|
||||||
|
(previous, current) => previous.query === current.query && previous.page === current.page,
|
||||||
|
),
|
||||||
|
tap(({ query }) => {
|
||||||
|
this.query.set(query);
|
||||||
|
this.results.set(null);
|
||||||
|
this.error.set(
|
||||||
|
query.length >= this.minSearchLength
|
||||||
|
? null
|
||||||
|
: `Ingresá al menos ${this.minSearchLength} caracteres para buscar.`,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
routeState$,
|
||||||
|
this.catalogAvailabilityService.availabilityChanged$.pipe(startWith(undefined)),
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([routeState]) => routeState),
|
||||||
switchMap(({ query, page }) => {
|
switchMap(({ query, page }) => {
|
||||||
if (query.length < this.minSearchLength) {
|
if (query.length < this.minSearchLength) {
|
||||||
return of(null);
|
return of(null);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
CatalogFeaturedItem,
|
CatalogFeaturedItem,
|
||||||
} from '../../../../core/services/catalog/catalog.interface';
|
} from '../../../../core/services/catalog/catalog.interface';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { Tenant } from '../../../../core/services/tenant.interface';
|
import { Tenant } from '../../../../core/services/tenant.interface';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
@@ -78,12 +79,14 @@ function provideActivatedRoute(catalogData: StoreHomeCatalogResolvedData) {
|
|||||||
const pageOneItems: CatalogFeaturedItem[] = [
|
const pageOneItems: CatalogFeaturedItem[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
type: 'product',
|
||||||
nombre: 'Auriculares Bluetooth',
|
nombre: 'Auriculares Bluetooth',
|
||||||
precio: '24999.00',
|
precio: '24999.00',
|
||||||
image: '/catalog/auriculares.jpg',
|
image: '/catalog/auriculares.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
|
type: 'product',
|
||||||
nombre: 'Teclado Mecanico',
|
nombre: 'Teclado Mecanico',
|
||||||
precio: '18999.00',
|
precio: '18999.00',
|
||||||
image: null,
|
image: null,
|
||||||
@@ -159,6 +162,30 @@ describe('StoreHomePageComponent', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reloads the catalog when availability changes', async () => {
|
||||||
|
const refreshedCatalog = createCatalog();
|
||||||
|
const catalogServiceStub = {
|
||||||
|
getCatalog: vi.fn().mockReturnValue(of(refreshedCatalog)),
|
||||||
|
getFeaturedGroupItems: vi.fn(),
|
||||||
|
withCustomLoading: vi.fn(),
|
||||||
|
};
|
||||||
|
catalogServiceStub.withCustomLoading.mockReturnValue(catalogServiceStub);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [StoreHomePageComponent],
|
||||||
|
providers: [
|
||||||
|
provideActivatedRoute({ response: createCatalog(), error: null }),
|
||||||
|
{ provide: CatalogService, useValue: catalogServiceStub },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(StoreHomePageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
TestBed.inject(CatalogAvailabilityService).notifyAvailabilityChanged();
|
||||||
|
|
||||||
|
expect(catalogServiceStub.getCatalog).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the carousel URLs received in tenant extras', async () => {
|
it('renders the carousel URLs received in tenant extras', async () => {
|
||||||
const carousel = ['https://example.com/carousel-1.webp', 'https://example.com/carousel-2.webp'];
|
const carousel = ['https://example.com/carousel-1.webp', 'https://example.com/carousel-2.webp'];
|
||||||
|
|
||||||
@@ -378,7 +405,9 @@ describe('StoreHomePageComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('requests another page for the selected featured group', async () => {
|
it('requests another page for the selected featured group', async () => {
|
||||||
const pageTwoItems = [{ id: 3, nombre: 'Mouse Gamer', precio: '15999.00', image: null }];
|
const pageTwoItems: CatalogFeaturedItem[] = [
|
||||||
|
{ id: 3, type: 'product', nombre: 'Mouse Gamer', precio: '15999.00', image: null },
|
||||||
|
];
|
||||||
const catalogServiceStub = {
|
const catalogServiceStub = {
|
||||||
getCatalog: vi.fn(),
|
getCatalog: vi.fn(),
|
||||||
getFeaturedGroupItems: vi.fn().mockReturnValue(of(createItemsPage(pageTwoItems, 2, 2))),
|
getFeaturedGroupItems: vi.fn().mockReturnValue(of(createItemsPage(pageTwoItems, 2, 2))),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { finalize, Subscription } from 'rxjs';
|
|||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
import { CatalogFeaturedGroup } from '../../../../core/services/catalog/catalog.interface';
|
import { CatalogFeaturedGroup } from '../../../../core/services/catalog/catalog.interface';
|
||||||
|
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
|
||||||
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
})
|
})
|
||||||
export class StoreHomePageComponent implements OnInit, OnDestroy {
|
export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
|
private readonly catalogAvailabilityService = inject(CatalogAvailabilityService);
|
||||||
private readonly catalogService = inject(CatalogService);
|
private readonly catalogService = inject(CatalogService);
|
||||||
private readonly injector = inject(Injector);
|
private readonly injector = inject(Injector);
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
@@ -96,9 +98,13 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly hasMainCarouselImages = computed(() => this.mainCarouselImages().length > 0);
|
protected readonly hasMainCarouselImages = computed(() => this.mainCarouselImages().length > 0);
|
||||||
|
|
||||||
private catalogRequestSubscription: Subscription | null = null;
|
private catalogRequestSubscription: Subscription | null = null;
|
||||||
|
private availabilityChangedSubscription: Subscription | null = null;
|
||||||
private readonly groupRequestSubscriptions = new Map<number, Subscription>();
|
private readonly groupRequestSubscriptions = new Map<number, Subscription>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.availabilityChangedSubscription =
|
||||||
|
this.catalogAvailabilityService.availabilityChanged$.subscribe(() => this.loadCatalog(true));
|
||||||
|
|
||||||
const resolvedData = this.route.snapshot.data['catalogData'] as
|
const resolvedData = this.route.snapshot.data['catalogData'] as
|
||||||
| StoreHomeCatalogResolvedData
|
| StoreHomeCatalogResolvedData
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -112,6 +118,7 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.availabilityChangedSubscription?.unsubscribe();
|
||||||
this.catalogRequestSubscription?.unsubscribe();
|
this.catalogRequestSubscription?.unsubscribe();
|
||||||
this.groupRequestSubscriptions.forEach((subscription) => subscription.unsubscribe());
|
this.groupRequestSubscriptions.forEach((subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
@@ -264,20 +271,27 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadCatalog(): void {
|
private loadCatalog(silent = false): void {
|
||||||
this.loading.set(true);
|
if (!silent) {
|
||||||
this.error.set(null);
|
this.loading.set(true);
|
||||||
|
this.error.set(null);
|
||||||
|
}
|
||||||
this.catalogRequestSubscription?.unsubscribe();
|
this.catalogRequestSubscription?.unsubscribe();
|
||||||
|
|
||||||
this.catalogRequestSubscription = this.catalogService
|
this.catalogRequestSubscription = this.catalogService
|
||||||
.withCustomLoading()
|
.withCustomLoading()
|
||||||
.getCatalog()
|
.getCatalog()
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (catalog) => this.catalog.set(catalog),
|
next: (catalog) => {
|
||||||
|
this.catalog.set(catalog);
|
||||||
|
this.error.set(null);
|
||||||
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.catalog.set([]);
|
if (!silent) {
|
||||||
|
this.catalog.set([]);
|
||||||
|
this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.error.set(STORE_HOME_PRODUCTS_ERROR_MESSAGE);
|
|
||||||
},
|
},
|
||||||
complete: () => this.loading.set(false),
|
complete: () => this.loading.set(false),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user