260 lines
12 KiB
TypeScript
260 lines
12 KiB
TypeScript
import { provideHttpClient } from '@angular/common/http';
|
|||
|
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||
|
|
import { type ComponentFixture, TestBed } from '@angular/core/testing';
|
||
|
|
import { ActivatedRoute, Router, convertToParamMap, provideRouter } from '@angular/router';
|
||
|
|
import { of } from 'rxjs';
|
||
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||
|
|
import { MasterFormPage } from './master-form.page';
|
||
|
|
import { BASE_MASTER_SCREENS } from './master-screens';
|
||
|
|
|
||
|
|
const customer = (over: Record<string, unknown> = {}) => ({
|
||
|
|
'odata.etag': 'W/"3"',
|
||
|
|
CardCode: 'C100',
|
||
|
|
CardName: 'ACME Ltda',
|
||
|
|
CardType: 'cCustomer',
|
||
|
|
FederalTaxID: '76111222-3',
|
||
|
|
CardForeignName: null,
|
||
|
|
GroupCode: 100,
|
||
|
|
Currency: 'CLP',
|
||
|
|
Phone1: '2 2345 6789',
|
||
|
|
Phone2: null,
|
||
|
|
Cellular: null,
|
||
|
|
EmailAddress: 'ventas@acme.cl',
|
||
|
|
Website: null,
|
||
|
|
ContactPerson: null,
|
||
|
|
PayTermsGrpCode: 1,
|
||
|
|
PriceListNum: 1,
|
||
|
|
CreditLimit: 5000,
|
||
|
|
DiscountPercent: 0,
|
||
|
|
Frozen: 'tNO',
|
||
|
|
Notes: null,
|
||
|
|
...over,
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('MasterFormPage', () => {
|
||
|
|
let fixture: ComponentFixture<MasterFormPage>;
|
||
|
|
let ctrl: HttpTestingController;
|
||
|
|
let navigate: ReturnType<typeof vi.spyOn>;
|
||
|
|
const el = () => fixture.nativeElement as HTMLElement;
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
const page = () => fixture.componentInstance as any;
|
||
|
|
const tick = async () => {
|
||
|
|
fixture.detectChanges();
|
||
|
|
await new Promise((r) => setTimeout(r));
|
||
|
|
fixture.detectChanges();
|
||
|
|
};
|
||
|
|
const labels = () => [...el().querySelectorAll('label')].map((l) => l.textContent?.trim());
|
||
|
|
const buttons = () => [...el().querySelectorAll('.actions .btn')].map((b) => b.textContent?.trim());
|
||
|
|
|
||
|
|
async function open(path: string, key: string | null, doc?: Record<string, unknown>) {
|
||
|
|
const screen = BASE_MASTER_SCREENS.find((s) => s.path === path)!;
|
||
|
|
const params = key === null ? {} : { key };
|
||
|
|
const route = {
|
||
|
|
snapshot: { data: { screen, title: screen.singular }, paramMap: convertToParamMap(params) },
|
||
|
|
paramMap: of(convertToParamMap(params)),
|
||
|
|
};
|
||
|
|
await TestBed.configureTestingModule({
|
||
|
|
imports: [MasterFormPage],
|
||
|
|
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
|
||
|
|
}).compileComponents();
|
||
|
|
ctrl = TestBed.inject(HttpTestingController);
|
||
|
|
navigate = vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true);
|
||
|
|
fixture = TestBed.createComponent(MasterFormPage);
|
||
|
|
await tick();
|
||
|
|
if (key !== null) ctrl.expectOne(`/api/sl/${screen.resource}/${encodeURIComponent(key)}`).flush(doc ?? {});
|
||
|
|
await tick();
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(() => TestBed.resetTestingModule());
|
||
|
|
|
||
|
|
describe('un cliente existente', () => {
|
||
|
|
beforeEach(() => open('/socios/clientes', 'C100', customer()));
|
||
|
|
|
||
|
|
it('muestra el código y el nombre, el estado y las pestañas con sus campos', () => {
|
||
|
|
expect(el().querySelector('.title')?.textContent).toContain('Cliente · C100');
|
||
|
|
expect(el().querySelector('.badge.tone-ok')?.textContent?.trim()).toBe('Activo');
|
||
|
|
expect([...el().querySelectorAll('[role=tab]')].map((t) => t.textContent?.trim())).toEqual(['General', 'Condiciones de pago', 'Observaciones']);
|
||
|
|
expect(labels()).toEqual(expect.arrayContaining(['Código', 'Nombre', 'RUT', 'Teléfono 1', 'Correo electrónico', 'Condición de pago', 'Límite de crédito']));
|
||
|
|
const values = [...el().querySelectorAll<HTMLInputElement>('input')].map((i) => i.value);
|
||
|
|
expect(values).toEqual(expect.arrayContaining(['C100', 'ACME Ltda', '76111222-3', 'ventas@acme.cl']));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('el código no se puede cambiar', () => {
|
||
|
|
const code = [...el().querySelectorAll<HTMLInputElement>('input')].find((i) => i.value === 'C100')!;
|
||
|
|
expect(code.readOnly).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sin cambios, Actualizar no envía nada', async () => {
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
ctrl.expectNone((r) => r.method === 'PATCH');
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('No hay cambios');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('envía solo lo que cambió, con If-Match, y recarga', async () => {
|
||
|
|
page().setValue('Phone1', '9 8765 4321');
|
||
|
|
page().setValue('CreditLimit', 8000);
|
||
|
|
await tick();
|
||
|
|
expect(page().dirty()).toBe(true);
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
const req = ctrl.expectOne((r) => r.method === 'PATCH');
|
||
|
|
expect(req.request.url).toBe('/api/sl/BusinessPartners/C100');
|
||
|
|
expect(req.request.headers.get('If-Match')).toBe('W/"3"');
|
||
|
|
expect(req.request.body).toEqual({ Phone1: '9 8765 4321', CreditLimit: 8000 });
|
||
|
|
req.flush(null);
|
||
|
|
await tick();
|
||
|
|
ctrl.expectOne('/api/sl/BusinessPartners/C100').flush(customer({ Phone1: '9 8765 4321', CreditLimit: 8000 }));
|
||
|
|
await tick();
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('Cliente actualizado');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('un campo vaciado se envía como null para que SAP lo limpie', async () => {
|
||
|
|
page().setValue('EmailAddress', '');
|
||
|
|
await tick();
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
expect(ctrl.expectOne((r) => r.method === 'PATCH').request.body).toEqual({ EmailAddress: null });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('un 412 avisa del conflicto, no guarda y ofrece Recargar', async () => {
|
||
|
|
page().setValue('CardName', 'Otro nombre');
|
||
|
|
await tick();
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
ctrl.expectOne((r) => r.method === 'PATCH').flush({ message: 'x' }, { status: 412, statusText: 'Precondition Failed' });
|
||
|
|
await tick();
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('Otro usuario modificó');
|
||
|
|
expect(buttons()).toContain('Recargar');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('la navegación pide el vecino por clave de texto y abre su registro', async () => {
|
||
|
|
const done = page().go('next');
|
||
|
|
const req = ctrl.expectOne((r) => r.url === '/api/sl/BusinessPartners' && r.params.has('$orderby'));
|
||
|
|
expect(req.request.params.get('$filter')).toBe(`(CardType eq 'cCustomer') and (CardCode gt 'C100')`);
|
||
|
|
expect(req.request.params.get('$orderby')).toBe('CardCode asc');
|
||
|
|
req.flush({ value: [{ CardCode: 'C200' }] });
|
||
|
|
await done;
|
||
|
|
expect(navigate).toHaveBeenCalledWith(['..', 'C200'], expect.anything());
|
||
|
|
});
|
||
|
|
|
||
|
|
it('al llegar al extremo avisa y no navega', async () => {
|
||
|
|
const done = page().go('next');
|
||
|
|
ctrl.expectOne((r) => r.url === '/api/sl/BusinessPartners').flush({ value: [] });
|
||
|
|
await done;
|
||
|
|
await tick();
|
||
|
|
expect(navigate).not.toHaveBeenCalled();
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('No hay más registros');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('un cliente inactivo se muestra como tal', async () => {
|
||
|
|
await open('/socios/clientes', 'C100', customer({ Frozen: 'tYES' }));
|
||
|
|
expect(el().querySelector('.badge.tone-muted')?.textContent?.trim()).toBe('Inactivo');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('una clave con espacios y puntos se codifica en la URL (artículos)', async () => {
|
||
|
|
await open('/inventario/articulos', '3MG30080714 M 10.5', {
|
||
|
|
ItemCode: '3MG30080714 M 10.5',
|
||
|
|
ItemName: 'Cloud X 5',
|
||
|
|
Frozen: 'tNO',
|
||
|
|
InventoryItem: 'tYES',
|
||
|
|
QuantityOnStock: 12,
|
||
|
|
});
|
||
|
|
expect(el().querySelector('.title')?.textContent).toContain('Artículo · 3MG30080714 M 10.5');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('las casillas de un artículo muestran y cambian tYES / tNO', async () => {
|
||
|
|
await open('/inventario/articulos', 'A1', { 'odata.etag': 'W/"1"', ItemCode: 'A1', ItemName: 'Pieza', Frozen: 'tNO', InventoryItem: 'tYES', SalesItem: 'tNO', PurchaseItem: 'tYES' });
|
||
|
|
const boxes = [...el().querySelectorAll<HTMLInputElement>('input[type=checkbox]')];
|
||
|
|
expect(boxes.map((b) => b.checked)).toEqual(expect.arrayContaining([true, false]));
|
||
|
|
page().setValue('SalesItem', 'tYES');
|
||
|
|
await tick();
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
expect(ctrl.expectOne((r) => r.method === 'PATCH').request.body).toEqual({ SalesItem: 'tYES' });
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('un cliente nuevo', () => {
|
||
|
|
beforeEach(() => open('/socios/clientes', null));
|
||
|
|
|
||
|
|
it('arranca vacío, con el código editable y el botón Crear', () => {
|
||
|
|
expect(el().querySelector('.badge.tone-info')?.textContent?.trim()).toBe('Nuevo');
|
||
|
|
expect(buttons()).toContain('Crear');
|
||
|
|
const code = el().querySelector<HTMLInputElement>('input')!;
|
||
|
|
expect(code.readOnly).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('no envía nada sin código o sin nombre', async () => {
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
ctrl.expectNone((r) => r.method === 'POST');
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('Indique el código');
|
||
|
|
page().setValue('CardCode', 'C300');
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('Indique el nombre');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('crea el cliente con su tipo, sin campos vacíos, y abre el creado', async () => {
|
||
|
|
page().setValue('CardCode', 'C300');
|
||
|
|
page().setValue('CardName', 'Nuevo SpA');
|
||
|
|
page().setValue('GroupCode', 100);
|
||
|
|
await tick();
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
const req = ctrl.expectOne((r) => r.method === 'POST');
|
||
|
|
expect(req.request.url).toBe('/api/sl/BusinessPartners');
|
||
|
|
expect(req.request.body).toEqual({
|
||
|
|
CardCode: 'C300',
|
||
|
|
CardName: 'Nuevo SpA',
|
||
|
|
GroupCode: 100,
|
||
|
|
Frozen: 'tNO',
|
||
|
|
CreditLimit: 0,
|
||
|
|
DiscountPercent: 0,
|
||
|
|
CardType: 'cCustomer',
|
||
|
|
});
|
||
|
|
req.flush({ CardCode: 'C300' });
|
||
|
|
await tick();
|
||
|
|
expect(navigate).toHaveBeenCalledWith(['..', 'C300'], expect.objectContaining({ state: { created: 'C300' } }));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('un proveedor se crea con el tipo de proveedor', async () => {
|
||
|
|
TestBed.resetTestingModule();
|
||
|
|
await open('/socios/proveedores', null);
|
||
|
|
page().setValue('CardCode', 'P1');
|
||
|
|
page().setValue('CardName', 'Proveedor Uno');
|
||
|
|
await tick();
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
expect((ctrl.expectOne((r) => r.method === 'POST').request.body as Record<string, unknown>)['CardType']).toBe('cSupplier');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('muestra el error de SAP si rechaza el registro', async () => {
|
||
|
|
page().setValue('CardCode', 'C300');
|
||
|
|
page().setValue('CardName', 'Nuevo SpA');
|
||
|
|
await tick();
|
||
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||
|
|
await tick();
|
||
|
|
ctrl.expectOne((r) => r.method === 'POST').flush({ message: 'Código duplicado' }, { status: 400, statusText: 'Bad Request' });
|
||
|
|
await tick();
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('Código duplicado');
|
||
|
|
expect(navigate).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('un registro que no existe lo avisa', async () => {
|
||
|
|
const screen = BASE_MASTER_SCREENS.find((s) => s.path === '/socios/clientes')!;
|
||
|
|
const route = { snapshot: { data: { screen, title: 'Cliente' }, paramMap: convertToParamMap({ key: 'NOPE' }) }, paramMap: of(convertToParamMap({ key: 'NOPE' })) };
|
||
|
|
await TestBed.configureTestingModule({
|
||
|
|
imports: [MasterFormPage],
|
||
|
|
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
|
||
|
|
}).compileComponents();
|
||
|
|
ctrl = TestBed.inject(HttpTestingController);
|
||
|
|
fixture = TestBed.createComponent(MasterFormPage);
|
||
|
|
await tick();
|
||
|
|
ctrl.expectOne('/api/sl/BusinessPartners/NOPE').flush({}, { status: 404, statusText: 'Not Found' });
|
||
|
|
await tick();
|
||
|
|
expect(el().querySelector('.status')?.textContent).toContain('El registro no existe');
|
||
|
|
});
|
||
|
|
});
|