test(catalog): cover availability-driven controls

This commit is contained in:
2026-08-24 14:33:25 -03:00
parent 5ca1f03e20
commit eda8d32f4e
7 changed files with 118 additions and 43 deletions

View File

@@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing';
import { beforeEach, describe, expect, it } from 'vitest';
import { ProductAttribute } from '../../../../core/services/catalog/catalog.interface';
import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability';
import { ProductAttributeSelectorComponent } from './product-attribute-selector.component';
describe('ProductAttributeSelectorComponent', () => {
@@ -31,7 +32,7 @@ describe('ProductAttributeSelectorComponent', () => {
fixture.componentRef.setInput('variants', [
{
id: 1,
maximum_addable_quantity: null,
availability: createCatalogAvailability(null),
values: { size: 'S' },
},
]);
@@ -51,12 +52,12 @@ describe('ProductAttributeSelectorComponent', () => {
fixture.componentRef.setInput('variants', [
{
id: 1,
maximum_addable_quantity: 0,
availability: createCatalogAvailability(0),
values: { size: 'S' },
},
{
id: 2,
maximum_addable_quantity: 2,
availability: createCatalogAvailability(2),
values: { size: 'M' },
},
]);
@@ -92,9 +93,9 @@ describe('ProductAttributeSelectorComponent', () => {
]);
fixture.componentRef.setInput('inventoryPolicy', 'unlimited');
fixture.componentRef.setInput('variants', [
{ id: 1, maximum_addable_quantity: null, values: { event_date: '1' } },
{ id: 2, maximum_addable_quantity: null, values: { event_date: '2' } },
{ id: 3, maximum_addable_quantity: null, values: { event_date: ['1', '2'] } },
{ id: 1, availability: createCatalogAvailability(null), values: { event_date: '1' } },
{ id: 2, availability: createCatalogAvailability(null), values: { event_date: '2' } },
{ id: 3, availability: createCatalogAvailability(null), values: { event_date: ['1', '2'] } },
]);
fixture.detectChanges();
@@ -135,8 +136,16 @@ describe('ProductAttributeSelectorComponent', () => {
]);
fixture.componentRef.setInput('inventoryPolicy', 'unlimited');
fixture.componentRef.setInput('variants', [
{ id: 1, maximum_addable_quantity: null, values: { size: 'S', internal_type: 'adult' } },
{ id: 2, maximum_addable_quantity: null, values: { size: 'M', internal_type: 'child' } },
{
id: 1,
availability: createCatalogAvailability(null),
values: { size: 'S', internal_type: 'adult' },
},
{
id: 2,
availability: createCatalogAvailability(null),
values: { size: 'M', internal_type: 'child' },
},
]);
fixture.detectChanges();

View File

@@ -10,6 +10,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { CatalogItemDetail } from '../../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability';
import { CartService } from '../../../../core/services/cart/cart.service';
import { ToastService } from '../../../../core/services/toast.service';
import { ProductDetailPageComponent } from './product-detail-page.component';
@@ -38,7 +39,7 @@ describe('ProductDetailPageComponent', () => {
has_tickets: false,
minimum_use_date: null,
maximum_use_date: null,
maximum_addable_quantity: 10,
availability: createCatalogAvailability(10),
attributes: [],
variants: [],
};
@@ -179,7 +180,7 @@ describe('ProductDetailPageComponent', () => {
it('reloads product availability when the cart changes', async () => {
catalogServiceStub.getCatalogItem.mockReturnValue(
of({ ...mockProduct, maximum_addable_quantity: 4 }),
of({ ...mockProduct, availability: createCatalogAvailability(4) }),
);
await configureTestingModule();
const fixture = TestBed.createComponent(ProductDetailPageComponent);
@@ -207,10 +208,10 @@ describe('ProductDetailPageComponent', () => {
const detailProduct: CatalogItemDetail = {
...mockProduct,
images: ['https://example.com/product.png'],
variants: [{ id: 123, maximum_addable_quantity: 10, values: {} }],
variants: [{ id: 123, availability: createCatalogAvailability(10), values: {} }],
selected_variant: {
id: 123,
maximum_addable_quantity: 10,
availability: createCatalogAvailability(10),
images: ['https://example.com/variant1.png', 'https://example.com/variant2.png'],
values: {},
},
@@ -312,11 +313,15 @@ describe('ProductDetailPageComponent', () => {
},
],
variants: [
{ id: 123, maximum_addable_quantity: 10, values: { color: 'beige', material: 'Cuero' } },
{
id: 123,
availability: createCatalogAvailability(10),
values: { color: 'beige', material: 'Cuero' },
},
],
selected_variant: {
id: 123,
maximum_addable_quantity: 10,
availability: createCatalogAvailability(10),
images: ['https://example.com/variant1.png'],
values: {
color: 'beige',
@@ -353,8 +358,18 @@ describe('ProductDetailPageComponent', () => {
purpose: 'entry',
has_tickets: true,
variants: [
{ id: 101, event_date_id: 20, maximum_addable_quantity: 10, values: { event_date: '20' } },
{ id: 102, event_date_id: 21, maximum_addable_quantity: 10, values: { event_date: '21' } },
{
id: 101,
event_date_id: 20,
availability: createCatalogAvailability(10),
values: { event_date: '20' },
},
{
id: 102,
event_date_id: 21,
availability: createCatalogAvailability(10),
values: { event_date: '21' },
},
],
attributes: [
{
@@ -385,7 +400,7 @@ describe('ProductDetailPageComponent', () => {
selected_variant: {
id: 101,
event_date_id: 20,
maximum_addable_quantity: 10,
availability: createCatalogAvailability(10),
images: [],
values: {},
},
@@ -407,6 +422,8 @@ describe('ProductDetailPageComponent', () => {
options[1].click();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(catalogServiceStub.getCatalogItem).toHaveBeenCalledWith(1, 102);
});
@@ -429,7 +446,7 @@ describe('ProductDetailPageComponent', () => {
fixture.componentInstance['selectedVariant'].set({
id: 1,
maximum_addable_quantity: 10,
availability: createCatalogAvailability(10),
values: {},
});
fixture.detectChanges();
@@ -519,7 +536,7 @@ describe('ProductDetailPageComponent', () => {
error: {
code: 'purchase.limit_exceeded',
message: 'Podés agregar hasta 2 unidades más de “Auriculares Bluetooth”.',
maximum_addable_quantity: 2,
availability: createCatalogAvailability(2),
},
}),
);
@@ -544,13 +561,13 @@ describe('ProductDetailPageComponent', () => {
variants: [
{
id: 123,
maximum_addable_quantity: 5,
availability: createCatalogAvailability(5),
values: {},
},
],
selected_variant: {
id: 123,
maximum_addable_quantity: 5,
availability: createCatalogAvailability(5),
images: [],
values: {},
},
@@ -583,7 +600,7 @@ describe('ProductDetailPageComponent', () => {
resolveProduct({
...mockProduct,
variants: [],
maximum_addable_quantity: 4,
availability: createCatalogAvailability(4),
});
await configureTestingModule();
@@ -609,13 +626,13 @@ describe('ProductDetailPageComponent', () => {
variants: [
{
id: 123,
maximum_addable_quantity: 5,
availability: createCatalogAvailability(5),
values: {},
},
],
selected_variant: {
id: 123,
maximum_addable_quantity: 5,
availability: createCatalogAvailability(5),
images: [],
values: {},
},
@@ -647,7 +664,7 @@ describe('ProductDetailPageComponent', () => {
it('allows unlimited variants to increase quantity without a maximum', async () => {
const unlimitedVariant = {
id: 321,
maximum_addable_quantity: null,
availability: createCatalogAvailability(null),
values: {},
};
resolveProduct({
@@ -655,7 +672,7 @@ describe('ProductDetailPageComponent', () => {
inventory_policy: 'unlimited',
selected_variant: {
id: 321,
maximum_addable_quantity: null,
availability: createCatalogAvailability(null),
images: [],
values: {},
},
@@ -679,7 +696,7 @@ describe('ProductDetailPageComponent', () => {
it('caps an unlimited variant at the per-user purchase limit', async () => {
const unlimitedVariant = {
id: 322,
maximum_addable_quantity: 2,
availability: createCatalogAvailability(2),
values: {},
};
resolveProduct({
@@ -688,7 +705,7 @@ describe('ProductDetailPageComponent', () => {
max_units_per_user: 2,
selected_variant: {
id: 322,
maximum_addable_quantity: 2,
availability: createCatalogAvailability(2),
images: [],
values: { event_date: '20' },
},
@@ -712,14 +729,14 @@ describe('ProductDetailPageComponent', () => {
it('disables purchase actions for tracked variants without stock', async () => {
const trackedVariant = {
id: 654,
maximum_addable_quantity: 0,
availability: createCatalogAvailability(0),
values: {},
};
resolveProduct({
...mockProduct,
selected_variant: {
id: 654,
maximum_addable_quantity: 0,
availability: createCatalogAvailability(0),
images: [],
values: {},
},

View File

@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CatalogItemDetail } from '../../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability';
import {
PRODUCT_DETAIL_ERROR_MESSAGE,
PRODUCT_DETAIL_INVALID_ID_MESSAGE,
@@ -29,7 +30,7 @@ describe('productDetailResolver', () => {
has_tickets: false,
minimum_use_date: null,
maximum_use_date: null,
maximum_addable_quantity: 0,
availability: createCatalogAvailability(0),
attributes: [],
variants: [],
};

View File

@@ -13,6 +13,7 @@ import {
} from '../../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability';
import { Tenant } from '../../../../core/services/tenant.interface';
import { TenantService } from '../../../../core/services/tenant.service';
import { ToastService } from '../../../../core/services/toast.service';
@@ -83,6 +84,7 @@ const pageOneItems: CatalogFeaturedItem[] = [
nombre: 'Auriculares Bluetooth',
precio: '24999.00',
image: '/catalog/auriculares.jpg',
availability: createCatalogAvailability(null),
},
{
id: 2,
@@ -90,6 +92,7 @@ const pageOneItems: CatalogFeaturedItem[] = [
nombre: 'Teclado Mecanico',
precio: '18999.00',
image: null,
availability: createCatalogAvailability(null),
},
];
@@ -406,7 +409,14 @@ describe('StoreHomePageComponent', () => {
it('requests another page for the selected featured group', async () => {
const pageTwoItems: CatalogFeaturedItem[] = [
{ id: 3, type: 'product', nombre: 'Mouse Gamer', precio: '15999.00', image: null },
{
id: 3,
type: 'product',
nombre: 'Mouse Gamer',
precio: '15999.00',
image: null,
availability: createCatalogAvailability(null),
},
];
const catalogServiceStub = {
getCatalog: vi.fn(),

View File

@@ -6,6 +6,7 @@ import { of } from 'rxjs';
import { ApiPaginatedResponse } from '../../../core/services/api-paginated-response.interface';
import { CartService } from '../../../core/services/cart/cart.service';
import { CatalogService } from '../../../core/services/catalog/catalog.service';
import { createCatalogAvailability } from '../../../core/services/catalog/catalog-availability';
import {
CatalogFeaturedItems,
CatalogGroupLayout,
@@ -23,6 +24,7 @@ describe('ProductListComponent', () => {
descripcion: 'Primera descripcion',
precio: '100.00',
image: '/images/one.png',
availability: createCatalogAvailability(null),
variants: [],
},
{
@@ -32,6 +34,7 @@ describe('ProductListComponent', () => {
descripcion: 'Segunda descripcion',
precio: 200,
image: null,
availability: createCatalogAvailability(null),
variants: [],
},
];
@@ -193,8 +196,8 @@ describe('ProductListComponent', () => {
const itemWithVariants: ProductListItem = {
...items[0],
variants: [
{ id: 91, maximum_addable_quantity: 3, values: { fecha: '10 de octubre' } },
{ id: 92, maximum_addable_quantity: 4, values: { fecha: '11 de octubre' } },
{ id: 91, availability: createCatalogAvailability(3), values: { fecha: '10 de octubre' } },
{ id: 92, availability: createCatalogAvailability(4), values: { fecha: '11 de octubre' } },
],
};
const fixture = await render('column_with_cart', [itemWithVariants]);
@@ -220,7 +223,7 @@ describe('ProductListComponent', () => {
variants: [
{
id: 91,
maximum_addable_quantity: 2,
availability: createCatalogAvailability(2),
values: { fecha: '10 de octubre' },
},
],
@@ -248,7 +251,7 @@ describe('ProductListComponent', () => {
variants: [
{
id: 91,
maximum_addable_quantity: 0,
availability: createCatalogAvailability(0),
values: { fecha: '10 de octubre' },
},
],
@@ -304,7 +307,7 @@ describe('ProductListComponent', () => {
{
id: 401,
precio: '10000.00',
maximum_addable_quantity: 1,
availability: createCatalogAvailability(1),
values: {
tipo: { value: 'vip', label: 'VIP' },
sector: { value: 'a', label: 'Sector A' },
@@ -315,7 +318,7 @@ describe('ProductListComponent', () => {
{
id: 402,
precio: '12000.00',
maximum_addable_quantity: 1,
availability: createCatalogAvailability(1),
values: {
tipo: { value: 'vip', label: 'VIP' },
sector: { value: 'a', label: 'Sector A' },

View File

@@ -58,11 +58,22 @@ describe('ProductVerticalWithCartCardComponent', () => {
it('shows the backend availability message with the reusable tooltip', async () => {
const fixture = await createComponent();
fixture.componentRef.setInput('maximumAddableQuantity', 0);
fixture.componentRef.setInput(
'unavailableMessage',
'Alcanzaste el cupo máximo permitido para este producto.',
);
fixture.componentRef.setInput('availability', {
maximum_quantity: 0,
restrictions: [
{
code: 'user_quota_reached',
message: 'Alcanzaste el cupo máximo permitido para este producto.',
scope: 'product',
},
],
capabilities: {
select_variant: false,
change_quantity: false,
add_to_cart: false,
buy_now: false,
},
});
fixture.detectChanges();
const tooltip = (fixture.nativeElement as HTMLElement).querySelector('app-tooltip');

View File

@@ -62,6 +62,8 @@ describe('VariantSelectorComponent', () => {
]);
fixture.componentRef.setInput('selectedVariant', 2);
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const selects = Array.from(
fixture.nativeElement.querySelectorAll('select'),
@@ -80,6 +82,28 @@ describe('VariantSelectorComponent', () => {
expect(fixture.componentInstance.selectedVariant()).toBe(1);
});
it('does not offer variants whose availability forbids selection', async () => {
await TestBed.configureTestingModule({
imports: [VariantSelectorComponent],
}).compileComponents();
const fixture = TestBed.createComponent(VariantSelectorComponent);
fixture.componentRef.setInput('variants', [
{ id: 1, values: { talle: 'S' } },
{
id: 2,
values: { talle: 'M' },
availability: { capabilities: { select_variant: false } },
},
]);
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('S');
expect(fixture.nativeElement.textContent).not.toContain('M');
expect(fixture.componentInstance.selectedVariant()).toBe(1);
});
it('requires manual selections when autoSelectFirst is disabled', async () => {
await TestBed.configureTestingModule({
imports: [VariantSelectorComponent],