Compare commits

..

2 Commits

7 changed files with 97 additions and 146 deletions

View File

@@ -6,7 +6,6 @@ import { provideRouter } from '@angular/router';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { App } from './app';
import { EventDateNoticeService } from './core/services/event-date-notice.service';
import { Tenant } from './core/services/tenant.interface';
import { TenantService } from './core/services/tenant.service';
@@ -117,48 +116,4 @@ describe('App', () => {
).toBe("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E");
});
it('shows the date notices returned by the tenant bootstrap once', async () => {
const show = vi.fn();
const dateNotices = [
{
type: 'suspended' as const,
title: 'FECHA CANCELADA!',
message: [
{ text: 'La fecha del ', bold: false },
{ text: '09 de Octubre de 2026', bold: true },
{ text: ' ha sido cancelada.', bold: false },
],
},
];
await TestBed.configureTestingModule({
imports: [App],
providers: [
provideRouter([]),
{
provide: TenantService,
useValue: createTenantServiceStub('ready', {
...tenant,
event: {
title: 'Festival',
location: 'Predio',
dates: [],
date_notices: dateNotices,
},
}),
},
{
provide: EventDateNoticeService,
useValue: { show },
},
],
}).compileComponents();
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.detectChanges();
expect(show).toHaveBeenCalledOnce();
expect(show).toHaveBeenCalledWith(dateNotices);
});
});

View File

@@ -1,9 +1,8 @@
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import { Component, PLATFORM_ID, computed, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Component, computed, effect, inject } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { RouterOutlet } from '@angular/router';
import { EventDateNoticeService } from './core/services/event-date-notice.service';
import { TenantService } from './core/services/tenant.service';
import { DEFAULT_TENANT_BRANDING } from './core/services/tenant-ssr-cache.store';
import { GlobalLoadingComponent } from './shared/components/global-loading/global-loading.component';
@@ -59,11 +58,8 @@ function hexToRgb(hex: string): string {
})
export class App {
private readonly tenantService = inject(TenantService);
private readonly eventDateNoticeService = inject(EventDateNoticeService);
private readonly document = inject(DOCUMENT);
private readonly title = inject(Title);
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
private notifiedTenantCode: string | null = null;
constructor() {
effect(() => {
@@ -82,11 +78,6 @@ export class App {
}
favicon.setAttribute('href', faviconHref);
if (this.isBrowser && tenant && this.notifiedTenantCode !== tenant.codigo) {
this.notifiedTenantCode = tenant.codigo;
this.eventDateNoticeService.show(tenant.event?.date_notices ?? []);
}
});
}

View File

@@ -1,85 +1,59 @@
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
import { Subject } from 'rxjs';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { EventDateNoticeModalComponent } from '../../shared/components/event-date-notice-modal/event-date-notice-modal.component';
import { EventDateNoticeService } from './event-date-notice.service';
import { ModalService } from './modal.service';
import { TenantEventDateNotice } from './tenant.interface';
import { environment } from '../../../environments/environment';
import { EventDateNotice, EventDateNoticeService } from './event-date-notice.service';
import { ApiResponse } from './api-response.interface';
import { TenantService } from './tenant.service';
describe('EventDateNoticeService', () => {
let service: EventDateNoticeService;
let open: ReturnType<typeof vi.fn>;
beforeAll(() => {
try {
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
} catch {
// Test environment may already be initialized by another setup entrypoint.
}
});
let httpMock: HttpTestingController;
beforeEach(() => {
open = vi.fn();
TestBed.configureTestingModule({
providers: [
EventDateNoticeService,
provideHttpClient(),
provideHttpClientTesting(),
{
provide: ModalService,
useValue: { open },
provide: TenantService,
useValue: { getTenant: () => ({ codigo: 'festival' }) },
},
],
});
service = TestBed.inject(EventDateNoticeService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
TestBed.resetTestingModule();
});
afterEach(() => httpMock.verify());
it('shows backend notices sequentially', () => {
const firstClosed = new Subject<void | undefined>();
const secondClosed = new Subject<void | undefined>();
const notices: TenantEventDateNotice[] = [
it('claims pending notices for the active tenant', () => {
const response = {
data: [
{
type: 'suspended',
type: 'suspended' as const,
change_ids: [10],
title: 'FECHA CANCELADA!',
message: [{ text: 'La fecha fue cancelada.', bold: false }],
},
{
type: 'rescheduled',
title: 'FECHA REPROGRAMADA!',
message: [{ text: 'La fecha fue reprogramada.', bold: false }],
},
];
],
};
let result: ApiResponse<EventDateNotice[]> | undefined;
open
.mockReturnValueOnce({ afterClosed$: firstClosed.asObservable() })
.mockReturnValueOnce({ afterClosed$: secondClosed.asObservable() });
service.claim().subscribe((value) => (result = value));
service.show(notices);
const request = httpMock.expectOne(
`${environment.url}tenants/festival/event-date-notices/claim`,
);
expect(request.request.method).toBe('POST');
expect(request.request.body).toEqual({});
expect(open).toHaveBeenCalledTimes(1);
expect(open).toHaveBeenNthCalledWith(1, EventDateNoticeModalComponent, {
size: 'sm',
data: notices[0],
});
request.flush(response);
firstClosed.next(undefined);
expect(open).toHaveBeenCalledTimes(2);
expect(open).toHaveBeenNthCalledWith(2, EventDateNoticeModalComponent, {
size: 'sm',
data: notices[1],
});
});
it('does not open a modal without notices', () => {
service.show([]);
expect(open).not.toHaveBeenCalled();
expect(result).toEqual(response);
});
});

View File

@@ -73,34 +73,10 @@ export interface ActiveEventDate {
isCanceled: boolean;
}
export type TenantEventDateChangeType = 'rescheduled' | 'suspended';
export interface TenantEventDateChange {
type: TenantEventDateChangeType;
source_event_date_id: number | null;
destination_event_date_id: number | null;
previous_date: string;
new_date: string | null;
occurred_at: string;
}
export interface TenantEventDateNoticeMessagePart {
text: string;
bold: boolean;
}
export interface TenantEventDateNotice {
type: TenantEventDateChangeType;
title: string;
message: TenantEventDateNoticeMessagePart[];
}
export interface TenantEvent {
title: string;
location: string;
dates: ActiveEventDate[];
date_changes?: TenantEventDateChange[];
date_notices?: TenantEventDateNotice[];
}
export interface WebsiteExtras {

View File

@@ -31,8 +31,9 @@
class="cart-item-variant-selector"
[variants]="variants()"
[selectedVariant]="selectedVariant()"
[autoSelectFirst]="false"
[compact]="true"
(selectedVariantChange)="onVariantChange($event)"
(selectionValuesChange)="onVariantChange($event.selectedVariant)"
/>
} @else {
<div class="d-grid cart-item-attributes">

View File

@@ -3,13 +3,14 @@ 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 { of, Subject, 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 { CartComponent } from './cart.component';
import { VariantSelectorComponent } from '../variant-selector/variant-selector.component';
describe('CartComponent', () => {
beforeAll(() => {
@@ -22,6 +23,7 @@ describe('CartComponent', () => {
afterEach(() => {
TestBed.resetTestingModule();
vi.restoreAllMocks();
});
it('shows an empty cart message when there are no items', async () => {
@@ -565,6 +567,57 @@ describe('CartComponent', () => {
expect(quantityChange).not.toHaveBeenCalled();
});
it('does not automatically replace an unavailable variant or retry a rejected selection', async () => {
const response = new Subject<unknown>();
const updateItemVariant = vi.fn().mockReturnValue(response);
const danger = vi.fn();
vi.spyOn(console, 'error').mockImplementation(() => {});
await TestBed.configureTestingModule({
imports: [CartComponent],
providers: [
{ provide: CartService, useValue: { cart: signal(null), updateItemVariant } },
{ provide: ModalService, useValue: {} },
{ provide: ToastService, useValue: { success: vi.fn(), danger } },
],
}).compileComponents();
const fixture = TestBed.createComponent(CartComponent);
const item = {
cartItemId: 10,
imageUrl: null,
product: 'Entrada',
originalPrice: null,
discountedPrice: 1000,
discountPercentage: null,
attributes: [],
quantity: 2,
variantId: 20,
variants: [{ id: 20, values: { fecha: '09/10' } }],
};
fixture.componentRef.setInput('items', [item]);
fixture.detectChanges();
const rescheduledItem = {
...item, variants: [{ id: 21, values: { fecha: '20/10' } }],
};
fixture.componentRef.setInput('items', [rescheduledItem]);
fixture.detectChanges();
expect(updateItemVariant).not.toHaveBeenCalled();
const selector = fixture.debugElement.query(By.directive(VariantSelectorComponent));
(selector.componentInstance as any).onValueChange('fecha', '20/10');
fixture.detectChanges();
expect(updateItemVariant).toHaveBeenCalledExactlyOnceWith(10, 2, 21);
response.error({ status: 422, error: { message: 'Variante no disponible.' } });
fixture.detectChanges();
fixture.componentRef.setInput('items', [{
...rescheduledItem, variants: [...rescheduledItem.variants],
}]);
fixture.detectChanges();
expect(updateItemVariant).toHaveBeenCalledTimes(1);
expect(danger).toHaveBeenCalledOnce();
});
it('persists a variant selected from a cart row', async () => {
const updateItemVariant = vi.fn().mockReturnValue(
of({

View File

@@ -3,8 +3,8 @@ import { TestBed, getTestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { EventDateNotice } from '../../../core/services/event-date-notice.service';
import { MODAL_DATA, ModalRef } from '../../../core/services/modal.service';
import { TenantEventDateNotice } from '../../../core/services/tenant.interface';
import { EventDateNoticeModalComponent } from './event-date-notice-modal.component';
describe('EventDateNoticeModalComponent', () => {
@@ -22,8 +22,9 @@ describe('EventDateNoticeModalComponent', () => {
it('renders the precalculated message and emphasizes its marked parts', async () => {
const close = vi.fn();
const data: TenantEventDateNotice = {
const data: EventDateNotice = {
type: 'rescheduled',
change_ids: [1, 2],
title: 'FECHAS REPROGRAMADAS!',
message: [
{ text: 'Las fechas del ', bold: false },
@@ -48,7 +49,7 @@ describe('EventDateNoticeModalComponent', () => {
expect(element.querySelector('h2')?.textContent).toContain(data.title);
expect(
[...element.querySelectorAll('strong')].map((strong) => strong.textContent?.trim()),
[...element.querySelectorAll('b')].map((strong) => strong.textContent?.trim()),
).toEqual(['09 y 10 de Octubre de 2026', 'respectivamente']);
element.querySelector('button')?.click();