429 lines
22 KiB
TypeScript
429 lines
22 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 { CLIENT_EXTENSIONS } from './client-extensions';
|
|
import { fieldGroup, headerKeysOf } from './document-layouts';
|
|
import { lineFieldsOf } from './line-columns';
|
|
import { DocumentFormPage } from './document-form.page';
|
|
import { BASE_SCREENS, DOCUMENT_SCREENS } from './document-screens';
|
|
import { type ScreenExtensions, applyExtensions } from './screen-extensions';
|
|
|
|
const by = (screens: readonly { path: string }[], path: string) => screens.find((s) => s.path === path)! as (typeof BASE_SCREENS)[number];
|
|
|
|
const EXT: ScreenExtensions = {
|
|
'/ventas/facturas': {
|
|
headerRight: [{ type: 'text', key: 'U_Obra', label: 'Obra' }],
|
|
footerLeft: [{ type: 'text', key: 'U_Nota', label: 'Nota interna' }],
|
|
lineColumns: [{ key: 'U_Lote', title: 'Lote', width: '90px' }],
|
|
listColumns: [{ key: 'U_Obra', title: 'Obra', searchable: true }],
|
|
},
|
|
'/inventario/entradas': { headerLeft: [{ type: 'text', key: 'U_Chofer', label: 'Chofer' }], lineColumns: [{ key: 'U_Origen', title: 'Origen' }] },
|
|
};
|
|
|
|
describe('extensiones por cliente', () => {
|
|
it('en main no agregan nada: las pantallas son las del base', () => {
|
|
expect(CLIENT_EXTENSIONS).toEqual({});
|
|
expect(DOCUMENT_SCREENS).toEqual(BASE_SCREENS);
|
|
});
|
|
|
|
it('agregan campos de cabecera y de pie al final y los envían a SAP', () => {
|
|
const s = by(applyExtensions(BASE_SCREENS, EXT), '/ventas/facturas');
|
|
expect(s.layout.header.right.at(-1)).toMatchObject({ key: 'U_Obra' });
|
|
expect(s.layout.footer.left.at(-1)).toMatchObject({ key: 'U_Nota' });
|
|
expect(headerKeysOf(s.layout)).toEqual(expect.arrayContaining(['U_Obra', 'U_Nota']));
|
|
});
|
|
|
|
it('agregan columnas de línea al final (después de las base) y las envían; y columnas de lista al final', () => {
|
|
const s = by(applyExtensions(BASE_SCREENS, EXT), '/ventas/facturas');
|
|
const keys = s.layout.columns.map((c) => c.key);
|
|
expect(keys.at(-1)).toBe('U_Lote');
|
|
expect(keys.indexOf('LineTotal')).toBeLessThan(keys.indexOf('U_Lote'));
|
|
expect(s.layout.lines.fields).toContain('U_Lote');
|
|
expect(s.columns.at(-1)).toMatchObject({ key: 'U_Obra' });
|
|
});
|
|
|
|
it('en un documento sin total las columnas van al final', () => {
|
|
const s = by(applyExtensions(BASE_SCREENS, EXT), '/inventario/entradas');
|
|
expect(s.layout.columns.at(-1)).toMatchObject({ key: 'U_Origen' });
|
|
});
|
|
|
|
it('solo cambia la pantalla extendida (aunque comparta layout con otra)', () => {
|
|
const out = applyExtensions(BASE_SCREENS, EXT);
|
|
expect(by(out, '/inventario/salidas')).toEqual(by(BASE_SCREENS, '/inventario/salidas'));
|
|
expect(by(out, '/ventas/ordenes')).toEqual(by(BASE_SCREENS, '/ventas/ordenes'));
|
|
expect(by(BASE_SCREENS, '/ventas/facturas').layout.columns.map((c) => c.key)).not.toContain('U_Lote');
|
|
});
|
|
|
|
it('una ruta que no existe es un error', () => {
|
|
expect(() => applyExtensions(BASE_SCREENS, { '/ventas/facturaz': { listColumns: [] } })).toThrow(/no existe la pantalla/);
|
|
});
|
|
});
|
|
|
|
describe('indicador y folio juntos', () => {
|
|
it('la factura del base lleva Indicador, prefijo y número en una sola fila, en ese orden', () => {
|
|
const layout = by(BASE_SCREENS, '/ventas/facturas').layout;
|
|
const groups = layout.header.right.filter((f) => f.type === 'group');
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0].type === 'group' && groups[0].cells.map((c) => c.key)).toEqual(['Indicator', 'FolioPrefixString', 'FolioNumber']);
|
|
expect(layout.header.left.some((f) => 'key' in f && f.key === 'Indicator')).toBe(false); // ya no va suelto
|
|
});
|
|
|
|
it('lo llevan facturas, boletas, reservas y notas de crédito de ventas y compras', () => {
|
|
for (const path of ['/ventas/facturas', '/ventas/boletas', '/ventas/facturas-reserva', '/ventas/boletas-reserva', '/ventas/notas-credito', '/compras/facturas', '/compras/notas-credito']) {
|
|
expect(by(BASE_SCREENS, path).layout.header.right.some((f) => f.type === 'group'), path).toBe(true);
|
|
}
|
|
expect(by(BASE_SCREENS, '/ventas/ordenes').layout.header.right.some((f) => f.type === 'group')).toBe(false);
|
|
});
|
|
|
|
it('una empresa con campos de usuario los sustituye por su propio grupo, también juntos', () => {
|
|
const screens = applyExtensions(BASE_SCREENS, {
|
|
'/ventas/facturas': {
|
|
hideHeaderFields: ['Indicator'], // oculta el grupo completo del base
|
|
headerRight: [
|
|
fieldGroup('Tipo / Serie / Número', [
|
|
{ kind: 'text', key: 'U_Tipo', label: 'Tipo', width: '60px' },
|
|
{ kind: 'text', key: 'U_Serie', label: 'Serie', width: '80px' },
|
|
{ kind: 'text', key: 'U_Numero', label: 'Número' },
|
|
]),
|
|
],
|
|
},
|
|
});
|
|
const layout = by(screens, '/ventas/facturas').layout;
|
|
const groups = layout.header.right.filter((f) => f.type === 'group');
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0].type === 'group' && groups[0].cells.map((c) => c.key)).toEqual(['U_Tipo', 'U_Serie', 'U_Numero']);
|
|
expect(headerKeysOf(layout)).toEqual(expect.arrayContaining(['U_Tipo', 'U_Serie', 'U_Numero']));
|
|
expect(headerKeysOf(layout)).not.toContain('FolioNumber');
|
|
});
|
|
});
|
|
|
|
describe('formulario con campos de cliente', () => {
|
|
let fixture: ComponentFixture<DocumentFormPage>;
|
|
let ctrl: HttpTestingController;
|
|
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();
|
|
};
|
|
|
|
async function open(doc: Record<string, unknown>) {
|
|
const screen = by(
|
|
applyExtensions(BASE_SCREENS, {
|
|
'/ventas/ordenes': {
|
|
headerLeft: [
|
|
{ type: 'select', key: 'U_Canal', label: 'Canal', options: [{ value: 'W', label: 'Web' }, { value: 'T', label: 'Tienda' }] },
|
|
],
|
|
headerRight: [{ type: 'text', key: 'U_Obra', label: 'Obra' }],
|
|
lineColumns: [{ key: 'U_Lote', title: 'Lote' }],
|
|
lookupColumns: { partner: [{ key: 'U_Zona', title: 'Zona' }], item: [{ key: 'U_Marca', title: 'Marca' }] },
|
|
},
|
|
}),
|
|
'/ventas/ordenes',
|
|
);
|
|
const route = {
|
|
snapshot: { data: { screen, title: 'Órdenes de venta' }, paramMap: convertToParamMap({ docEntry: '5' }) },
|
|
paramMap: of(convertToParamMap({ docEntry: '5' })),
|
|
};
|
|
await TestBed.configureTestingModule({
|
|
imports: [DocumentFormPage],
|
|
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
|
|
}).compileComponents();
|
|
ctrl = TestBed.inject(HttpTestingController);
|
|
vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true);
|
|
fixture = TestBed.createComponent(DocumentFormPage);
|
|
await tick();
|
|
ctrl.expectOne((r) => r.url === '/api/sl/SalesTaxCodes').flush({ value: [] });
|
|
ctrl.expectOne('/api/sl/Orders/5').flush(doc);
|
|
await tick();
|
|
}
|
|
|
|
const order = {
|
|
'odata.etag': 'W/"1"',
|
|
DocEntry: 5,
|
|
DocNum: 5,
|
|
CardCode: 'C1',
|
|
CardName: 'ACME',
|
|
DocDate: '2026-09-11T00:00:00Z',
|
|
DocDueDate: '2026-09-20T00:00:00Z',
|
|
DiscountPercent: 0,
|
|
DocumentStatus: 'bost_Open',
|
|
Cancelled: 'tNO',
|
|
U_Canal: 'W',
|
|
U_Obra: 'Torre 3',
|
|
DocumentLines: [{ LineNum: 0, ItemCode: 'A1', ItemDescription: 'Pieza', Quantity: 1, Price: 10, DiscountPercent: 0, TaxCode: '', LineTotal: 10, WarehouseCode: '01', U_Lote: 'L-77' }],
|
|
};
|
|
|
|
beforeEach(() => TestBed.resetTestingModule());
|
|
|
|
it('muestra el campo de usuario, el desplegable y la columna de línea con sus valores de SAP', async () => {
|
|
await open(order);
|
|
expect([...el().querySelectorAll('label')].map((l) => l.textContent?.trim())).toEqual(expect.arrayContaining(['Canal', 'Obra']));
|
|
expect([...el().querySelectorAll<HTMLSelectElement>('select')].map((s) => s.value)).toContain('W');
|
|
expect([...el().querySelectorAll<HTMLInputElement>('input')].map((i) => i.value)).toEqual(expect.arrayContaining(['Torre 3', 'L-77']));
|
|
});
|
|
|
|
it('la ventana de artículos de una línea muestra las columnas del documento', async () => {
|
|
await open(order);
|
|
const itemBtn = el().querySelector('.pick button') as HTMLButtonElement;
|
|
itemBtn.click();
|
|
await tick();
|
|
const req = ctrl.expectOne((r) => r.url === '/api/sl/Items');
|
|
expect(req.request.params.get('$select')).toContain('U_Marca');
|
|
req.flush({ value: [{ ItemCode: 'A1', ItemName: 'Pieza', U_Marca: 'Acme' }], 'odata.count': '1' });
|
|
await tick();
|
|
expect([...el().querySelectorAll('.dlg thead th')].map((t) => t.textContent?.trim())).toEqual(expect.arrayContaining(['Código', 'Descripción', 'Marca']));
|
|
expect(el().querySelector('.dlg tbody')!.textContent).toContain('Acme');
|
|
});
|
|
|
|
it('al modificar envía solo el campo de usuario que cambió', async () => {
|
|
await open(order);
|
|
page().setHeader('U_Canal', 'T');
|
|
page().setHeader('U_Obra', 'Torre 4');
|
|
await tick();
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
|
await tick();
|
|
const req = ctrl.expectOne((r) => r.method === 'PATCH');
|
|
expect(req.request.body).toEqual({ U_Canal: 'T', U_Obra: 'Torre 4' });
|
|
req.flush(null);
|
|
await tick();
|
|
ctrl.expectOne('/api/sl/Orders/5').flush(order);
|
|
});
|
|
|
|
it('al cambiar la columna de línea envía las líneas con el campo de usuario', async () => {
|
|
await open(order);
|
|
page().setLines([{ ...page().lines()[0], U_Lote: 'L-80' }]);
|
|
await tick();
|
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
|
await tick();
|
|
const req = ctrl.expectOne((r) => r.method === 'PATCH');
|
|
expect((req.request.body as { DocumentLines: Record<string, unknown>[] }).DocumentLines[0]).toMatchObject({ ItemCode: 'A1', U_Lote: 'L-80' });
|
|
req.flush(null);
|
|
await tick();
|
|
ctrl.expectOne('/api/sl/Orders/5').flush(order);
|
|
});
|
|
});
|
|
|
|
describe('columnas de las listas de valores por documento', () => {
|
|
const find = (screens: readonly { path: string }[], path: string) => by(screens, path).layout;
|
|
const itemCols = (path: string, screens = BASE_SCREENS) =>
|
|
find(screens, path).columns.find((c) => c.key === 'ItemCode')!.lookup!.config.columns.map((c) => c.key);
|
|
|
|
it('por defecto todos los documentos muestran código y descripción del artículo', () => {
|
|
expect(itemCols('/ventas/ordenes')).toEqual(['ItemCode', 'ItemName']);
|
|
expect(find(BASE_SCREENS, '/ventas/ordenes').lookups).toBeUndefined();
|
|
});
|
|
|
|
it('una pantalla del base cambia las columnas de su lista sin afectar a las demás', async () => {
|
|
const { marketingLayout } = await import('./document-layouts');
|
|
const layout = marketingLayout({
|
|
partnerLabel: 'Cliente',
|
|
taxResource: 'SalesTaxCodes',
|
|
uom: 'sales',
|
|
lookups: {
|
|
item: [{ key: 'ItemCode', title: 'Código' }, { key: 'ItemName', title: 'Descripción' }, { key: 'QuantityOnStock', title: 'Stock', type: 'number' }],
|
|
partner: [{ key: 'CardCode', title: 'Código' }, { key: 'CardName', title: 'Nombre' }, { key: 'Phone1', title: 'Teléfono' }],
|
|
},
|
|
});
|
|
expect(layout.columns.find((c) => c.key === 'ItemCode')!.lookup!.config.columns.map((c) => c.key)).toEqual(['ItemCode', 'ItemName', 'QuantityOnStock']);
|
|
expect(layout.lookups!.partner!.map((c) => c.key)).toEqual(['CardCode', 'CardName', 'Phone1']);
|
|
// el recurso, el filtro y los campos extra de la lista no cambian
|
|
const cfg = layout.columns.find((c) => c.key === 'ItemCode')!.lookup!.config;
|
|
expect(cfg.resource).toBe('Items');
|
|
expect(cfg.extraFields).toEqual(expect.arrayContaining(['UoMGroupEntry']));
|
|
expect(itemCols('/ventas/facturas')).toEqual(['ItemCode', 'ItemName']);
|
|
});
|
|
|
|
it('una extensión de cliente suma columnas al final de las del base', () => {
|
|
const out = applyExtensions(BASE_SCREENS, {
|
|
'/ventas/ordenes': {
|
|
lookupColumns: {
|
|
item: [{ key: 'U_Marca', title: 'Marca' }],
|
|
partner: [{ key: 'U_Zona', title: 'Zona' }],
|
|
warehouse: [{ key: 'U_Sucursal', title: 'Sucursal' }],
|
|
},
|
|
},
|
|
});
|
|
expect(itemCols('/ventas/ordenes', out)).toEqual(['ItemCode', 'ItemName', 'U_Marca']);
|
|
const l = find(out, '/ventas/ordenes');
|
|
expect(l.lookups!.partner!.map((c) => c.key)).toEqual(['CardCode', 'CardName', 'U_Zona']);
|
|
expect(l.columns.find((c) => c.key === 'WarehouseCode')!.lookup!.config.columns.map((c) => c.key)).toEqual(['WarehouseCode', 'WarehouseName', 'U_Sucursal']);
|
|
expect(itemCols('/ventas/facturas', out)).toEqual(['ItemCode', 'ItemName']);
|
|
});
|
|
|
|
it('la lista de almacenes se aplica a las dos columnas de almacén de una transferencia', () => {
|
|
const out = applyExtensions(BASE_SCREENS, { '/inventario/transferencias': { lookupColumns: { warehouse: [{ key: 'U_Sucursal', title: 'Sucursal' }] } } });
|
|
const cols = find(out, '/inventario/transferencias').columns.filter((c) => c.lookup && /Warehouse/.test(c.key));
|
|
expect(cols).toHaveLength(2);
|
|
for (const c of cols) expect(c.lookup!.config.columns.at(-1)!.key).toBe('U_Sucursal');
|
|
});
|
|
});
|
|
|
|
describe('ocultar y dejar de solo lectura (extensiones de cliente)', () => {
|
|
const layoutOf = (out: readonly { path: string }[], path: string) => by(out, path).layout;
|
|
const apply = (e: ScreenExtensions) => applyExtensions(BASE_SCREENS, e);
|
|
const DIMS_2_5 = ['CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5'];
|
|
|
|
it('un cliente que solo quiere CostingCode oculta las otras dimensiones: no se muestran ni se envían', () => {
|
|
const l = layoutOf(apply({ '/ventas/facturas': { hideLineColumns: DIMS_2_5 } }), '/ventas/facturas');
|
|
const keys = l.columns.map((c) => c.key);
|
|
expect(keys).toContain('CostingCode');
|
|
for (const k of DIMS_2_5) {
|
|
expect(keys).not.toContain(k);
|
|
expect(l.lines.fields).not.toContain(k);
|
|
}
|
|
expect(l.lines.fields).toContain('CostingCode');
|
|
});
|
|
|
|
it('oculta también las columnas de lectura y de lista, sin tocar las demás pantallas', () => {
|
|
const out = apply({
|
|
'/ventas/facturas': { hideLineColumns: ['BaseType', 'BaseEntry', 'BaseLine', 'AccountCode'], hideListColumns: ['FolioPrefixString'] },
|
|
});
|
|
expect(layoutOf(out, '/ventas/facturas').columns.map((c) => c.key)).not.toEqual(expect.arrayContaining(['BaseType']));
|
|
expect(by(out, '/ventas/facturas').columns.map((c) => c.key)).not.toContain('FolioPrefixString');
|
|
expect(by(out, '/ventas/facturas').columns.map((c) => c.key)).toContain('FolioNumber');
|
|
expect(layoutOf(out, '/ventas/ordenes').columns.map((c) => c.key)).toEqual(layoutOf(BASE_SCREENS, '/ventas/ordenes').columns.map((c) => c.key));
|
|
});
|
|
|
|
it('oculta campos de cabecera: dejan de mostrarse y de enviarse', () => {
|
|
const l = layoutOf(apply({ '/ventas/ordenes': { hideHeaderFields: ['Indicator', 'Project', 'partnerTaxId'] } }), '/ventas/ordenes');
|
|
const keys = headerKeysOf(l);
|
|
expect(keys).not.toEqual(expect.arrayContaining(['Indicator']));
|
|
expect(keys).not.toContain('Project');
|
|
expect(l.header.left.map((f) => ('label' in f ? f.label : ''))).not.toContain('RUT');
|
|
expect(keys).toContain('NumAtCard');
|
|
});
|
|
|
|
it('el cliente puede ocultar una columna base y agregar la suya', () => {
|
|
const l = layoutOf(apply({ '/ventas/ordenes': { hideLineColumns: ['AccountCode'], lineColumns: [{ key: 'U_Lote', title: 'Lote' }] } }), '/ventas/ordenes');
|
|
const keys = l.columns.map((c) => c.key);
|
|
expect(keys).not.toContain('AccountCode');
|
|
expect(keys.at(-1)).toBe('U_Lote');
|
|
});
|
|
|
|
it('una columna de solo lectura se ve y no se edita, pero su valor sigue enviándose', () => {
|
|
const l = layoutOf(apply({ '/ventas/facturas': { readOnlyLineColumns: ['CostingCode', 'ProjectCode'] } }), '/ventas/facturas');
|
|
const col = (k: string) => l.columns.find((c) => c.key === k)!;
|
|
expect(col('CostingCode').locked).toBe(true);
|
|
expect(col('ProjectCode').locked).toBe(true);
|
|
expect(col('CostingCode2').locked).toBeUndefined();
|
|
expect(l.lines.fields).toEqual(expect.arrayContaining(['CostingCode', 'ProjectCode']));
|
|
expect(lineFieldsOf(l.columns)).toEqual(expect.arrayContaining(['CostingCode', 'ProjectCode']));
|
|
});
|
|
|
|
it('un campo de cabecera de solo lectura queda marcado y se sigue enviando', () => {
|
|
const l = layoutOf(apply({ '/ventas/ordenes': { readOnlyHeaderFields: ['Indicator', 'NumAtCard'] } }), '/ventas/ordenes');
|
|
expect(l.lockedKeys).toEqual(expect.arrayContaining(['Indicator', 'NumAtCard']));
|
|
expect(headerKeysOf(l)).toEqual(expect.arrayContaining(['Indicator', 'NumAtCard']));
|
|
});
|
|
|
|
it('no se puede ocultar lo obligatorio, y un nombre que no existe es un error', () => {
|
|
expect(() => apply({ '/ventas/ordenes': { hideLineColumns: ['ItemCode'] } })).toThrow(/obligatoria/);
|
|
expect(() => apply({ '/ventas/ordenes': { hideLineColumns: ['Quantity'] } })).toThrow(/obligatoria/);
|
|
expect(() => apply({ '/ventas/ordenes': { hideHeaderFields: ['CardCode'] } })).toThrow(/obligatorio/);
|
|
expect(() => apply({ '/inventario/entradas': { hideLineColumns: ['WarehouseCode'] } })).toThrow(/obligatoria/);
|
|
expect(() => apply({ '/ventas/ordenes': { hideLineColumns: ['CostingCode9'] } })).toThrow(/no tiene la columna de línea/);
|
|
expect(() => apply({ '/ventas/ordenes': { hideHeaderFields: ['Nada'] } })).toThrow(/no tiene el campo de cabecera/);
|
|
expect(() => apply({ '/ventas/ordenes': { readOnlyLineColumns: ['Nada'] } })).toThrow(/no tiene la columna de línea/);
|
|
expect(() => apply({ '/ventas/ordenes': { hideListColumns: ['Nada'] } })).toThrow(/no tiene la columna de lista/);
|
|
});
|
|
});
|
|
|
|
describe('formulario con campos ocultos o de solo lectura', () => {
|
|
let fixture: ComponentFixture<DocumentFormPage>;
|
|
const el = () => fixture.nativeElement as HTMLElement;
|
|
const tick = async () => {
|
|
fixture.detectChanges();
|
|
await new Promise((r) => setTimeout(r));
|
|
fixture.detectChanges();
|
|
};
|
|
|
|
async function openWith(ext: ScreenExtensions) {
|
|
TestBed.resetTestingModule();
|
|
const screen = by(applyExtensions(BASE_SCREENS, ext), '/ventas/ordenes');
|
|
const route = {
|
|
snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap({}) },
|
|
paramMap: of(convertToParamMap({})),
|
|
};
|
|
await TestBed.configureTestingModule({
|
|
imports: [DocumentFormPage],
|
|
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
|
|
}).compileComponents();
|
|
fixture = TestBed.createComponent(DocumentFormPage);
|
|
await tick();
|
|
}
|
|
const input = (label: string) => {
|
|
const f = [...el().querySelectorAll('sbo-field, sbo-text-field, sbo-lookup-field')].find((x) => x.querySelector('label')?.textContent?.trim() === label);
|
|
return f?.querySelector<HTMLInputElement>('input') ?? null;
|
|
};
|
|
|
|
it('un campo oculto no aparece en el formulario', async () => {
|
|
await openWith({ '/ventas/ordenes': { hideHeaderFields: ['Indicator'] } });
|
|
expect([...el().querySelectorAll('label')].map((l) => l.textContent?.trim())).not.toContain('Indicador');
|
|
expect([...el().querySelectorAll('label')].map((l) => l.textContent?.trim())).toContain('Proyecto');
|
|
});
|
|
|
|
it('un campo de solo lectura se ve pero no se puede escribir', async () => {
|
|
await openWith({ '/ventas/ordenes': { readOnlyHeaderFields: ['Indicator', 'Project'] } });
|
|
expect(input('Indicador')!.readOnly).toBe(true);
|
|
expect(input('Proyecto')!.readOnly).toBe(true);
|
|
expect(input('Nº referencia')!.readOnly).toBe(false);
|
|
const buttons = [...el().querySelectorAll('sbo-lookup-field')].find((f) => f.querySelector('label')?.textContent?.trim() === 'Proyecto')!.querySelector('button.sbo-btn') as HTMLButtonElement;
|
|
expect(buttons.disabled).toBe(true);
|
|
});
|
|
|
|
it('sin extensiones los campos se editan como siempre', async () => {
|
|
await openWith({});
|
|
expect(input('Indicador')!.readOnly).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('campos Sí/No (checkbox) en los documentos', () => {
|
|
it('un campo de usuario de tipo casilla se dibuja como casilla y guarda tYES / tNO', async () => {
|
|
TestBed.resetTestingModule();
|
|
const screen = by(
|
|
applyExtensions(BASE_SCREENS, { '/ventas/ordenes': { headerLeft: [{ type: 'checkbox', key: 'U_Urgente', label: 'Urgente' }] } }),
|
|
'/ventas/ordenes',
|
|
);
|
|
const route = {
|
|
snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap({ docEntry: '5' }) },
|
|
paramMap: of(convertToParamMap({ docEntry: '5' })),
|
|
};
|
|
await TestBed.configureTestingModule({
|
|
imports: [DocumentFormPage],
|
|
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
|
|
}).compileComponents();
|
|
const ctrl = TestBed.inject(HttpTestingController);
|
|
vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true);
|
|
const fixture = TestBed.createComponent(DocumentFormPage);
|
|
const tick = async () => {
|
|
fixture.detectChanges();
|
|
await new Promise((r) => setTimeout(r));
|
|
fixture.detectChanges();
|
|
};
|
|
await tick();
|
|
ctrl.match((r) => r.url === '/api/sl/SalesTaxCodes').forEach((r) => r.flush({ value: [] }));
|
|
ctrl.match((r) => r.url === '/api/sl/AdditionalExpenses').forEach((r) => r.flush({ value: [] }));
|
|
ctrl.match((r) => r.url.includes('SeriesService')).forEach((r) => r.flush({ value: [] }));
|
|
ctrl.expectOne('/api/sl/Orders/5').flush({
|
|
'odata.etag': 'W/"1"', DocEntry: 5, DocNum: 5, CardCode: 'C1', CardName: 'ACME', U_Urgente: 'tNO',
|
|
DocDate: '2026-09-11T00:00:00Z', DocDueDate: '2026-09-20T00:00:00Z', DocumentStatus: 'bost_Open', Cancelled: 'tNO',
|
|
DocumentLines: [{ LineNum: 0, ItemCode: 'A1', ItemDescription: 'Pieza', Quantity: 1, Price: 10, TaxCode: '', LineTotal: 10 }],
|
|
});
|
|
await tick();
|
|
const el = fixture.nativeElement as HTMLElement;
|
|
const box = [...el.querySelectorAll('sbo-checkbox-field')].find((f) => f.querySelector('label')?.textContent?.trim() === 'Urgente')!.querySelector('input[type=checkbox]') as HTMLInputElement;
|
|
expect(box.checked).toBe(false);
|
|
box.click();
|
|
await tick();
|
|
(el.querySelector('.actions .btn.main') as HTMLElement).click();
|
|
await tick();
|
|
expect(ctrl.expectOne((r) => r.method === 'PATCH').request.body).toEqual({ U_Urgente: 'tYES' });
|
|
});
|
|
});
|