feat: enhance product detail page to support default variant selection from query parameters
This commit is contained in:
@@ -35,9 +35,14 @@ export class CatalogService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProducto(id: number): Observable<ProductDetail> {
|
getProducto(id: number, defaultVariantId?: number): Observable<ProductDetail> {
|
||||||
|
const params =
|
||||||
|
defaultVariantId === undefined
|
||||||
|
? undefined
|
||||||
|
: new HttpParams({ fromObject: { default_variant: defaultVariantId } });
|
||||||
|
|
||||||
return this.http
|
return this.http
|
||||||
.get<ApiResponse<ProductDetail>>(`${this.tenantApiUrl}/productos/${id}`)
|
.get<ApiResponse<ProductDetail>>(`${this.tenantApiUrl}/productos/${id}`, { params })
|
||||||
.pipe(map((response) => response.data));
|
.pipe(map((response) => response.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ describe('ProductDetailPageComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let paramMapSubject: BehaviorSubject<any>;
|
let paramMapSubject: BehaviorSubject<any>;
|
||||||
|
let queryParamMapSubject: BehaviorSubject<any>;
|
||||||
let catalogServiceStub: any;
|
let catalogServiceStub: any;
|
||||||
let routerStub: any;
|
let routerStub: any;
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ describe('ProductDetailPageComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
paramMapSubject = new BehaviorSubject(convertToParamMap({ id: '1' }));
|
paramMapSubject = new BehaviorSubject(convertToParamMap({ id: '1' }));
|
||||||
|
queryParamMapSubject = new BehaviorSubject(convertToParamMap({}));
|
||||||
catalogServiceStub = {
|
catalogServiceStub = {
|
||||||
getProducto: vi.fn().mockReturnValue(of(mockProduct))
|
getProducto: vi.fn().mockReturnValue(of(mockProduct))
|
||||||
};
|
};
|
||||||
@@ -62,7 +64,10 @@ describe('ProductDetailPageComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
useValue: { paramMap: paramMapSubject.asObservable() }
|
useValue: {
|
||||||
|
paramMap: paramMapSubject.asObservable(),
|
||||||
|
queryParamMap: queryParamMapSubject.asObservable()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: CatalogService,
|
provide: CatalogService,
|
||||||
@@ -83,7 +88,7 @@ describe('ProductDetailPageComponent', () => {
|
|||||||
|
|
||||||
const element = fixture.nativeElement as HTMLElement;
|
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('app-product-carousel')).not.toBeNull();
|
||||||
expect(element.querySelector('.product-detail__title')?.textContent).toContain('Auriculares Bluetooth');
|
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.');
|
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 () => {
|
it('should use default variant images if present', async () => {
|
||||||
const detailProduct: ProductDetail = {
|
const detailProduct: ProductDetail = {
|
||||||
...mockProduct,
|
...mockProduct,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
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 { CatalogService } from '../../../../core/services/catalog/catalog.service';
|
||||||
import {
|
import {
|
||||||
@@ -90,17 +90,17 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.routeSub = this.route.paramMap.subscribe((params) => {
|
this.routeSub = combineLatest([this.route.paramMap, this.route.queryParamMap]).subscribe(
|
||||||
const idParam = params.get('id');
|
([params, queryParams]) => {
|
||||||
if (idParam) {
|
const id = this.parseIntegerParam(params.get('id'));
|
||||||
const id = Number(idParam);
|
if (id === null) {
|
||||||
if (Number.isInteger(id)) {
|
|
||||||
this.loadProduct(id);
|
|
||||||
} else {
|
|
||||||
this.error.set('ID de producto inválido');
|
this.error.set('ID de producto inválido');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadProduct(id, this.parseIntegerParam(queryParams.get('default_variant')) ?? undefined);
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -110,13 +110,13 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
this.clearMeasurementTimer();
|
this.clearMeasurementTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadProduct(id: number): void {
|
private loadProduct(id: number, defaultVariantId?: number): void {
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.error.set(null);
|
this.error.set(null);
|
||||||
this.product.set(null);
|
this.product.set(null);
|
||||||
|
|
||||||
this.productSub?.unsubscribe();
|
this.productSub?.unsubscribe();
|
||||||
this.productSub = this.catalogService.getProducto(id).subscribe({
|
this.productSub = this.catalogService.getProducto(id, defaultVariantId).subscribe({
|
||||||
next: (prod) => {
|
next: (prod) => {
|
||||||
this.product.set(prod);
|
this.product.set(prod);
|
||||||
this.initializeSelections(prod);
|
this.initializeSelections(prod);
|
||||||
@@ -240,6 +240,16 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
return null;
|
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 {
|
private normalizeText(value: string | null | undefined): string {
|
||||||
return (value ?? '')
|
return (value ?? '')
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
|
|||||||
Reference in New Issue
Block a user