From b7420c21109a73ab72a993772a2fadf051441087 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 30 Jun 2026 15:07:01 -0300 Subject: [PATCH] feat: enhance product detail page to support default variant selection from query parameters --- .../core/services/catalog/catalog.service.ts | 9 ++++-- .../product-detail-page.component.spec.ts | 29 +++++++++++++++-- .../product-detail-page.component.ts | 32 ++++++++++++------- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/app/core/services/catalog/catalog.service.ts b/src/app/core/services/catalog/catalog.service.ts index 3a40197..b7a31be 100644 --- a/src/app/core/services/catalog/catalog.service.ts +++ b/src/app/core/services/catalog/catalog.service.ts @@ -35,9 +35,14 @@ export class CatalogService { ); } - getProducto(id: number): Observable { + getProducto(id: number, defaultVariantId?: number): Observable { + const params = + defaultVariantId === undefined + ? undefined + : new HttpParams({ fromObject: { default_variant: defaultVariantId } }); + return this.http - .get>(`${this.tenantApiUrl}/productos/${id}`) + .get>(`${this.tenantApiUrl}/productos/${id}`, { params }) .pipe(map((response) => response.data)); } diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts index 6ac07c1..7bf3a87 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts @@ -31,6 +31,7 @@ describe('ProductDetailPageComponent', () => { }; let paramMapSubject: BehaviorSubject; + let queryParamMapSubject: BehaviorSubject; let catalogServiceStub: any; let routerStub: any; @@ -48,6 +49,7 @@ describe('ProductDetailPageComponent', () => { beforeEach(() => { vi.restoreAllMocks(); paramMapSubject = new BehaviorSubject(convertToParamMap({ id: '1' })); + queryParamMapSubject = new BehaviorSubject(convertToParamMap({})); catalogServiceStub = { getProducto: vi.fn().mockReturnValue(of(mockProduct)) }; @@ -62,7 +64,10 @@ describe('ProductDetailPageComponent', () => { providers: [ { provide: ActivatedRoute, - useValue: { paramMap: paramMapSubject.asObservable() } + useValue: { + paramMap: paramMapSubject.asObservable(), + queryParamMap: queryParamMapSubject.asObservable() + } }, { provide: CatalogService, @@ -83,7 +88,7 @@ describe('ProductDetailPageComponent', () => { const element = fixture.nativeElement as HTMLElement; - expect(catalogServiceStub.getProducto).toHaveBeenCalledWith(1); + expect(catalogServiceStub.getProducto).toHaveBeenCalledWith(1, undefined); expect(element.querySelector('app-product-carousel')).not.toBeNull(); expect(element.querySelector('.product-detail__title')?.textContent).toContain('Auriculares Bluetooth'); @@ -103,6 +108,26 @@ describe('ProductDetailPageComponent', () => { expect(element.textContent).toContain('No pudimos cargar los detalles del producto.'); }); + it('passes default_variant from the query params when loading the product', async () => { + queryParamMapSubject.next(convertToParamMap({ default_variant: '123' })); + + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + + expect(catalogServiceStub.getProducto).toHaveBeenCalledWith(1, 123); + }); + + it('ignores an invalid default_variant query param', async () => { + queryParamMapSubject.next(convertToParamMap({ default_variant: 'abc' })); + + await configureTestingModule(); + const fixture = TestBed.createComponent(ProductDetailPageComponent); + fixture.detectChanges(); + + expect(catalogServiceStub.getProducto).toHaveBeenCalledWith(1, undefined); + }); + it('should use default variant images if present', async () => { const detailProduct: ProductDetail = { ...mockProduct, diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts index 3bed3d4..2918d03 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts @@ -12,7 +12,7 @@ import { } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { combineLatest, Subscription } from 'rxjs'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { @@ -90,17 +90,17 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.routeSub = this.route.paramMap.subscribe((params) => { - const idParam = params.get('id'); - if (idParam) { - const id = Number(idParam); - if (Number.isInteger(id)) { - this.loadProduct(id); - } else { + this.routeSub = combineLatest([this.route.paramMap, this.route.queryParamMap]).subscribe( + ([params, queryParams]) => { + const id = this.parseIntegerParam(params.get('id')); + if (id === null) { this.error.set('ID de producto inválido'); + return; } + + this.loadProduct(id, this.parseIntegerParam(queryParams.get('default_variant')) ?? undefined); } - }); + ); } ngOnDestroy(): void { @@ -110,13 +110,13 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { this.clearMeasurementTimer(); } - private loadProduct(id: number): void { + private loadProduct(id: number, defaultVariantId?: number): void { this.loading.set(true); this.error.set(null); this.product.set(null); this.productSub?.unsubscribe(); - this.productSub = this.catalogService.getProducto(id).subscribe({ + this.productSub = this.catalogService.getProducto(id, defaultVariantId).subscribe({ next: (prod) => { this.product.set(prod); this.initializeSelections(prod); @@ -240,6 +240,16 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy { return null; } + + private parseIntegerParam(value: string | null): number | null { + if (!value) { + return null; + } + + const parsed = Number(value); + return Number.isInteger(parsed) ? parsed : null; + } + private normalizeText(value: string | null | undefined): string { return (value ?? '') .normalize('NFD')