273 lines
14 KiB
TypeScript
273 lines
14 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 { collectionPayload, collectionRowsOf, collectionsCreatePayload, isBlankRow, masterCollectionsOf } from './master-model';
|
|
import { BASE_MASTER_SCREENS } from './master-screens';
|
|
|
|
const screen = (path: string) => BASE_MASTER_SCREENS.find((s) => s.path === path)!;
|
|
const collection = (path: string, key: string) => masterCollectionsOf(screen(path).layout).find((c) => c.key === key)!;
|
|
|
|
const ADDR = { AddressName: 'BillTo', AddressType: 'bo_BillTo', Street: 'Calle 1', City: 'Santiago', Country: 'CL', BPCode: 'C1', RowNum: 0 };
|
|
const SHIP = { AddressName: 'Bodega', AddressType: 'bo_ShipTo', Street: 'Ruta 5', City: 'Rancagua', Country: 'CL', BPCode: 'C1', RowNum: 1 };
|
|
|
|
describe('tablas de los maestros: qué se envía', () => {
|
|
const addresses = collection('/socios/clientes', 'BPAddresses');
|
|
const prices = collection('/inventario/articulos', 'ItemPrices');
|
|
|
|
it('los clientes y proveedores tienen direcciones y contactos; el artículo, precios y stock por almacén', () => {
|
|
for (const p of ['/socios/clientes', '/socios/proveedores']) {
|
|
expect(masterCollectionsOf(screen(p).layout).map((c) => c.key)).toEqual(['BPAddresses', 'ContactEmployees']);
|
|
}
|
|
expect(masterCollectionsOf(screen('/inventario/articulos').layout).map((c) => c.key)).toEqual(['ItemPrices', 'ItemWarehouseInfoCollection']);
|
|
});
|
|
|
|
it('direcciones y contactos se envían completos (replace); los precios por filas (merge); el stock no se edita', () => {
|
|
expect(addresses.mode).toBe('replace');
|
|
expect(collection('/socios/clientes', 'ContactEmployees').mode).toBe('replace');
|
|
expect(prices.mode).toBe('merge');
|
|
expect(prices.rowKey).toBe('PriceList');
|
|
expect(collection('/inventario/articulos', 'ItemWarehouseInfoCollection').readonly).toBe(true);
|
|
});
|
|
|
|
it('sin cambios no se envía nada', () => {
|
|
expect(collectionPayload(addresses, [ADDR, SHIP], [{ ...ADDR }, { ...SHIP }])).toBeNull();
|
|
expect(collectionPayload(addresses, [], [])).toBeNull();
|
|
});
|
|
|
|
it('replace: al cambiar una celda se envía la colección completa, con las filas leídas y sus campos de SAP', () => {
|
|
const out = collectionPayload(addresses, [ADDR, SHIP], [{ ...ADDR, Street: 'Calle 2' }, { ...SHIP }])!;
|
|
expect(out).toHaveLength(2);
|
|
expect(out[0]).toMatchObject({ AddressName: 'BillTo', Street: 'Calle 2', BPCode: 'C1', RowNum: 0 }); // conserva lo que SAP trae
|
|
expect(out[1]).toEqual(SHIP);
|
|
});
|
|
|
|
it('replace: añadir, quitar o reordenar una fila cuenta como cambio; una fila vacía no', () => {
|
|
expect(collectionPayload(addresses, [ADDR], [ADDR, { AddressName: 'Nueva', AddressType: 'bo_ShipTo' }])).toHaveLength(2);
|
|
expect(collectionPayload(addresses, [ADDR, SHIP], [ADDR])).toEqual([ADDR]);
|
|
expect(collectionPayload(addresses, [ADDR], [])).toEqual([]);
|
|
expect(collectionPayload(addresses, [ADDR], [ADDR, { AddressName: '', Street: '' }])).toBeNull();
|
|
expect(isBlankRow(addresses, { AddressName: ' ', Street: 'x' })).toBe(true);
|
|
expect(isBlankRow(addresses, { AddressName: 'A' })).toBe(false);
|
|
});
|
|
|
|
it('merge: solo se envían las filas cambiadas, con su clave y los campos que cambiaron', () => {
|
|
const original = [
|
|
{ PriceList: 1, Price: 100, Currency: 'CLP', Factor: 1 },
|
|
{ PriceList: 2, Price: 50, Currency: 'CLP', Factor: 0.5 },
|
|
];
|
|
const out = collectionPayload(prices, original, [{ ...original[0], Price: 120 }, { ...original[1] }]);
|
|
expect(out).toEqual([{ PriceList: 1, Price: 120 }]);
|
|
expect(collectionPayload(prices, original, [{ ...original[0], Price: '100' }, { ...original[1] }])).toBeNull(); // 100 = '100'
|
|
});
|
|
|
|
it('merge: vaciar un precio lo envía como null; las columnas de solo lectura no cuentan', () => {
|
|
const original = [{ PriceList: 1, Price: 100, Currency: 'CLP', Factor: 1 }];
|
|
expect(collectionPayload(prices, original, [{ ...original[0], Price: '' }])).toEqual([{ PriceList: 1, Price: null }]);
|
|
expect(collectionPayload(prices, original, [{ ...original[0], Factor: 9 }])).toBeNull();
|
|
});
|
|
|
|
it('el stock por almacén, de solo lectura, nunca se envía', () => {
|
|
const stock = collection('/inventario/articulos', 'ItemWarehouseInfoCollection');
|
|
expect(collectionPayload(stock, [{ WarehouseCode: '01', InStock: 5 }], [{ WarehouseCode: '01', InStock: 99 }])).toBeNull();
|
|
});
|
|
|
|
it('al crear se envían las filas con datos y sin campos vacíos; las vacías y el stock no', () => {
|
|
const body = collectionsCreatePayload(screen('/socios/clientes').layout, {
|
|
BPAddresses: [{ AddressName: 'BillTo', AddressType: 'bo_BillTo', Street: 'Calle 1', City: '' }, { AddressName: '' }],
|
|
ContactEmployees: [],
|
|
});
|
|
expect(body).toEqual({ BPAddresses: [{ AddressName: 'BillTo', AddressType: 'bo_BillTo', Street: 'Calle 1' }] });
|
|
expect(collectionsCreatePayload(screen('/inventario/articulos').layout, { ItemPrices: [{ PriceList: 1, Price: 10 }], ItemWarehouseInfoCollection: [{ WarehouseCode: '01' }] })).toEqual({
|
|
ItemPrices: [{ PriceList: 1, Price: 10 }],
|
|
});
|
|
});
|
|
|
|
it('copia las filas de SAP para editarlas sin tocar el original', () => {
|
|
const doc = { BPAddresses: [ADDR] };
|
|
const rows = collectionRowsOf(doc, addresses);
|
|
rows[0]['Street'] = 'otra';
|
|
expect(doc.BPAddresses[0].Street).toBe('Calle 1');
|
|
expect(collectionRowsOf(undefined, addresses)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('formulario con tablas', () => {
|
|
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 save = () => (el().querySelector('.actions .btn.main') as HTMLElement).click();
|
|
|
|
async function open(path: string, key: string | null, doc?: Record<string, unknown>) {
|
|
const s = screen(path);
|
|
const params = key === null ? {} : { key };
|
|
const route = { snapshot: { data: { screen: s, title: s.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/${s.resource}/${encodeURIComponent(key)}`).flush(doc ?? {});
|
|
await tick();
|
|
}
|
|
|
|
const customer = () => ({
|
|
'odata.etag': 'W/"4"', CardCode: 'C1', CardName: 'ACME', CardType: 'cCustomer', Frozen: 'tNO',
|
|
BPAddresses: [{ ...ADDR }, { ...SHIP }],
|
|
ContactEmployees: [{ Name: 'Juan Perez', FirstName: 'Juan', E_Mail: 'j@x.cl', InternalCode: 39, CardCode: 'C1' }],
|
|
});
|
|
const item = () => ({
|
|
'odata.etag': 'W/"2"', ItemCode: 'A1', ItemName: 'Pieza', Frozen: 'tNO',
|
|
ItemPrices: [{ PriceList: 1, Price: 100, Currency: 'CLP', Factor: 1, BasePriceList: 1 }, { PriceList: 2, Price: 50, Currency: 'CLP', Factor: 0.5, BasePriceList: 1 }],
|
|
ItemWarehouseInfoCollection: [{ WarehouseCode: '01', InStock: 990, Committed: 2, Ordered: 1, MinimalStock: 0, MaximalStock: 0 }],
|
|
});
|
|
const cell = (tab: number, row: number, col: number): HTMLInputElement =>
|
|
el().querySelectorAll('sbo-tab')[tab].querySelectorAll('tbody tr')[row].querySelectorAll<HTMLInputElement>('input, select')[col] as HTMLInputElement;
|
|
|
|
beforeEach(() => TestBed.resetTestingModule());
|
|
|
|
it('muestra las direcciones y los contactos que trae SAP en sus pestañas', async () => {
|
|
await open('/socios/clientes', 'C1', customer());
|
|
const tabs = el().querySelectorAll('sbo-tab');
|
|
const addrRows = [...tabs[3].querySelectorAll('tbody tr')];
|
|
expect(addrRows).toHaveLength(2);
|
|
expect(addrRows[0].querySelector<HTMLInputElement>('input')!.value).toBe('BillTo');
|
|
expect(tabs[4].querySelector<HTMLInputElement>('tbody input')!.value).toBe('Juan Perez');
|
|
expect(page().dirty()).toBe(false);
|
|
});
|
|
|
|
it('editar una dirección envía la colección completa con el reemplazo de colecciones', async () => {
|
|
await open('/socios/clientes', 'C1', customer());
|
|
const street = cell(3, 0, 2);
|
|
street.value = 'Calle 99';
|
|
street.dispatchEvent(new Event('change'));
|
|
await tick();
|
|
expect(page().dirty()).toBe(true);
|
|
save();
|
|
await tick();
|
|
const req = ctrl.expectOne((r) => r.method === 'PATCH');
|
|
expect(req.request.headers.get('B1S-ReplaceCollectionsOnPatch')).toBe('true');
|
|
const body = req.request.body as { BPAddresses: Record<string, unknown>[] };
|
|
expect(Object.keys(body)).toEqual(['BPAddresses']);
|
|
expect(body.BPAddresses).toHaveLength(2);
|
|
expect(body.BPAddresses[0]).toMatchObject({ AddressName: 'BillTo', Street: 'Calle 99', RowNum: 0, BPCode: 'C1' });
|
|
expect(body.BPAddresses[1]).toEqual(SHIP);
|
|
req.flush(null);
|
|
await tick();
|
|
ctrl.expectOne('/api/sl/BusinessPartners/C1').flush(customer());
|
|
});
|
|
|
|
it('añadir una persona de contacto y quitar una dirección', async () => {
|
|
await open('/socios/clientes', 'C1', customer());
|
|
page().addRow(masterCollectionsOf(screen('/socios/clientes').layout)[1]);
|
|
await tick();
|
|
expect(el().querySelectorAll('sbo-tab')[4].querySelectorAll('tbody tr')).toHaveLength(2);
|
|
const name = cell(4, 1, 0);
|
|
name.value = 'Ana Soto';
|
|
name.dispatchEvent(new Event('change'));
|
|
(el().querySelectorAll('sbo-tab')[3].querySelectorAll('td.del button')[1] as HTMLElement).click(); // quita «Bodega»
|
|
await tick();
|
|
save();
|
|
await tick();
|
|
const req = ctrl.expectOne((r) => r.method === 'PATCH');
|
|
const body = req.request.body as { BPAddresses: unknown[]; ContactEmployees: Record<string, unknown>[] };
|
|
expect(body.BPAddresses).toHaveLength(1);
|
|
expect(body.ContactEmployees.map((c) => c['Name'])).toEqual(['Juan Perez', 'Ana Soto']);
|
|
expect(body.ContactEmployees[0]).toMatchObject({ InternalCode: 39 }); // la persona existente conserva su código de SAP
|
|
req.flush(null);
|
|
await tick();
|
|
ctrl.expectOne('/api/sl/BusinessPartners/C1').flush(customer());
|
|
});
|
|
|
|
it('una fila añadida y dejada vacía no se envía ni cuenta como cambio', async () => {
|
|
await open('/socios/clientes', 'C1', customer());
|
|
page().addRow(masterCollectionsOf(screen('/socios/clientes').layout)[0]);
|
|
await tick();
|
|
expect(page().dirty()).toBe(false);
|
|
save();
|
|
await tick();
|
|
ctrl.expectNone((r) => r.method === 'PATCH');
|
|
expect(el().querySelector('.status')?.textContent).toContain('No hay cambios');
|
|
});
|
|
|
|
it('un cliente nuevo con una dirección la envía en el POST', async () => {
|
|
await open('/socios/clientes', null);
|
|
page().setValue('CardCode', 'C9');
|
|
page().setValue('CardName', 'Nuevo');
|
|
page().addRow(masterCollectionsOf(screen('/socios/clientes').layout)[0]);
|
|
await tick();
|
|
const name = cell(3, 0, 0);
|
|
name.value = 'BillTo';
|
|
name.dispatchEvent(new Event('change'));
|
|
await tick();
|
|
save();
|
|
await tick();
|
|
const req = ctrl.expectOne((r) => r.method === 'POST');
|
|
expect((req.request.body as Record<string, unknown>)['BPAddresses']).toEqual([{ AddressName: 'BillTo', AddressType: 'bo_ShipTo' }]);
|
|
req.flush({ CardCode: 'C9' });
|
|
await tick();
|
|
expect(navigate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('precios: solo se envía la lista que cambió, sin reemplazar la colección', async () => {
|
|
await open('/inventario/articulos', 'A1', item());
|
|
const price = cell(4, 1, 2);
|
|
expect(price.value).toBe('50.00');
|
|
price.value = '777';
|
|
price.dispatchEvent(new Event('change'));
|
|
await tick();
|
|
save();
|
|
await tick();
|
|
const req = ctrl.expectOne((r) => r.method === 'PATCH');
|
|
expect(req.request.headers.has('B1S-ReplaceCollectionsOnPatch')).toBe(false);
|
|
expect(req.request.body).toEqual({ ItemPrices: [{ PriceList: 2, Price: 777 }] });
|
|
req.flush(null);
|
|
await tick();
|
|
ctrl.expectOne('/api/sl/Items/A1').flush(item());
|
|
});
|
|
|
|
it('precios: la lista y el factor se ven pero no se editan, y no se pueden añadir ni quitar filas', async () => {
|
|
await open('/inventario/articulos', 'A1', item());
|
|
const tab = el().querySelectorAll('sbo-tab')[4];
|
|
expect((tab.querySelector('tbody tr input') as HTMLInputElement).readOnly).toBe(true); // lista de precios
|
|
expect(tab.querySelectorAll('td.del button')).toHaveLength(0);
|
|
expect(tab.querySelector('.addrow')).toBeNull();
|
|
});
|
|
|
|
it('el stock por almacén se muestra completo y de solo lectura', async () => {
|
|
await open('/inventario/articulos', 'A1', item());
|
|
const tab = el().querySelectorAll('sbo-tab')[5];
|
|
const inputs = [...tab.querySelectorAll<HTMLInputElement>('tbody input')];
|
|
expect(inputs.map((i) => i.value)).toEqual(['01', '', '990', '2', '1', '0', '0']); // la segunda es el nombre del almacén (llega del catálogo)
|
|
expect(inputs.every((i) => i.readOnly)).toBe(true);
|
|
expect(tab.querySelector('.addrow')).toBeNull();
|
|
expect(page().dirty()).toBe(false);
|
|
});
|
|
|
|
it('un artículo con cambios en un campo y en los precios envía las dos cosas juntas', async () => {
|
|
await open('/inventario/articulos', 'A1', item());
|
|
page().setValue('ItemName', 'Pieza nueva');
|
|
const price = cell(4, 0, 2);
|
|
price.value = '120';
|
|
price.dispatchEvent(new Event('change'));
|
|
await tick();
|
|
save();
|
|
await tick();
|
|
expect(ctrl.expectOne((r) => r.method === 'PATCH').request.body).toEqual({ ItemName: 'Pieza nueva', ItemPrices: [{ PriceList: 1, Price: 120 }] });
|
|
});
|
|
});
|