You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vce/src/tests/housing-catalog.test.ts

509 lines
19 KiB

/**
* ============================================================================
* CATÁLOGO DE VIVIENDAS - TESTS
* ============================================================================
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { RuleEngine } from '../engine/rule-engine';
import {
CATALOGO_VIVIENDAS,
APARTAMENTO,
DUPLEX,
RULES,
IDS_TEST
} from '../tests/fixtures/housing-catalog.fixture';
const {
// Objetos
OBJ_APARTAMENTO, OBJ_DUPLEX,
// Secciones
SEC_SALON, SEC_BANO, SEC_ASEO, SEC_DORMITORIO1, SEC_DORMITORIO2, SEC_COCINA,
// Atributos
ATTR_CALIDAD, ATTR_SUELO, ATTR_SANITARIO, ATTR_REVEST_BANO,
ATTR_ENCIMERA, ATTR_PUERTA, ATTR_PARED,
// Opciones calidad
OPT_CALIDAD_ESTANDAR, OPT_CALIDAD_PREMIUM, OPT_CALIDAD_LUJO,
// Opciones suelo
OPT_SUELO_CERAMICA, OPT_SUELO_PARQUET, OPT_SUELO_MICROCEMENTO,
// Opciones baño
OPT_SANIT_BLANCO, OPT_SANIT_NEGRO,
OPT_REVEST_CERAMICA, OPT_REVEST_MICROCEMENTO, OPT_REVEST_MARMOL,
// Opciones cocina
OPT_ENCIMERA_GRANITO, OPT_ENCIMERA_MADERA,
// Opciones pared/puerta
OPT_PARED_BLANCO, OPT_PUERTA_LACADA
} = IDS_TEST;
// ============================================================================
// SETUP
// ============================================================================
let engine: RuleEngine;
const rules = Object.values(RULES);
beforeEach(() => {
engine = new RuleEngine();
});
// ============================================================================
// CATÁLOGO - ESTRUCTURA
// ============================================================================
describe('Catálogo - Estructura', () => {
it('tiene nombre y descripción i18n', () => {
expect(CATALOGO_VIVIENDAS.name).toHaveProperty('es');
expect(CATALOGO_VIVIENDAS.name).toHaveProperty('en');
});
it('contiene los 2 modelos de vivienda', () => {
expect(Object.keys(CATALOGO_VIVIENDAS.objects)).toHaveLength(2);
expect(CATALOGO_VIVIENDAS.objects[OBJ_APARTAMENTO]).toBeDefined();
expect(CATALOGO_VIVIENDAS.objects[OBJ_DUPLEX]).toBeDefined();
});
it('contiene todas las opciones', () => {
expect(Object.keys(CATALOGO_VIVIENDAS.options).length).toBeGreaterThan(0);
});
it('contiene las reglas de validación', () => {
expect(CATALOGO_VIVIENDAS.rules).toBeDefined();
expect(Object.keys(CATALOGO_VIVIENDAS.rules!).length).toBeGreaterThan(0);
});
it('todas las opciones referenciadas existen en el catálogo', () => {
for (const obj of Object.values(CATALOGO_VIVIENDAS.objects)) {
for (const section of Object.values(obj.sections)) {
for (const attr of section.attrs) {
if (attr.type === 'dynamic') {
for (const opt of attr.options) {
expect(
CATALOGO_VIVIENDAS.options[opt.optionId],
`Opción ${opt.optionId} no encontrada en catálogo`
).toBeDefined();
}
}
}
}
}
});
});
// ============================================================================
// APARTAMENTO - ESTRUCTURA
// ============================================================================
describe('Apartamento - Estructura', () => {
it('tiene 4 secciones: salón, baño, dormitorio y cocina', () => {
const secciones = Object.keys(APARTAMENTO.sections);
expect(secciones).toHaveLength(4);
expect(APARTAMENTO.sections[SEC_SALON]).toBeDefined();
expect(APARTAMENTO.sections[SEC_BANO]).toBeDefined();
expect(APARTAMENTO.sections[SEC_DORMITORIO1]).toBeDefined();
expect(APARTAMENTO.sections[SEC_COCINA]).toBeDefined();
});
it('NO tiene aseo ni segundo dormitorio', () => {
expect(APARTAMENTO.sections[SEC_ASEO]).toBeUndefined();
expect(APARTAMENTO.sections[SEC_DORMITORIO2]).toBeUndefined();
});
it('tiene 1 atributo global: calidad', () => {
expect(APARTAMENTO.attributes).toHaveLength(1);
expect(APARTAMENTO.attributes[0].id).toBe(ATTR_CALIDAD);
});
it('el orden de secciones es correcto', () => {
expect(APARTAMENTO.sectionOrder).toEqual([
SEC_SALON, SEC_COCINA, SEC_DORMITORIO1, SEC_BANO
]);
});
it('el baño tiene 6 m²', () => {
const bano = APARTAMENTO.sections[SEC_BANO];
const m2Attr = bano.attrs.find(a => a.type === 'fixed');
expect((m2Attr as any)?.value).toBe(6);
});
});
// ============================================================================
// DÚPLEX - ESTRUCTURA
// ============================================================================
describe('Dúplex - Estructura', () => {
it('tiene 5 secciones: salón, baño, aseo y 2 dormitorios', () => {
const secciones = Object.keys(DUPLEX.sections);
expect(secciones).toHaveLength(5);
expect(DUPLEX.sections[SEC_SALON]).toBeDefined();
expect(DUPLEX.sections[SEC_BANO]).toBeDefined();
expect(DUPLEX.sections[SEC_ASEO]).toBeDefined();
expect(DUPLEX.sections[SEC_DORMITORIO1]).toBeDefined();
expect(DUPLEX.sections[SEC_DORMITORIO2]).toBeDefined();
});
it('NO tiene cocina', () => {
expect(DUPLEX.sections[SEC_COCINA]).toBeUndefined();
});
it('el baño principal tiene más m² que el del apartamento', () => {
const banoDuplex = DUPLEX.sections[SEC_BANO];
const banoApartamento = APARTAMENTO.sections[SEC_BANO];
const m2Duplex = banoDuplex.attrs.find(a => a.type === 'fixed') as any;
const m2Apartamento = banoApartamento.attrs.find(a => a.type === 'fixed') as any;
expect(m2Duplex.value).toBeGreaterThan(m2Apartamento.value);
});
it('el dormitorio principal es más grande que el secundario', () => {
const dorm1 = DUPLEX.sections[SEC_DORMITORIO1];
const dorm2 = DUPLEX.sections[SEC_DORMITORIO2];
const m2Dorm1 = dorm1.attrs.find(a => a.type === 'fixed') as any;
const m2Dorm2 = dorm2.attrs.find(a => a.type === 'fixed') as any;
expect(m2Dorm1.value).toBeGreaterThan(m2Dorm2.value);
});
it('el aseo tiene menos opciones de revestimiento que el baño', () => {
const bano = DUPLEX.sections[SEC_BANO];
const aseo = DUPLEX.sections[SEC_ASEO];
const revestBano = bano.attrs.find(a => a.id === ATTR_REVEST_BANO) as any;
const revestAseo = aseo.attrs.find(a => a.id === ATTR_REVEST_BANO) as any;
expect(revestAseo.options.length).toBeLessThan(revestBano.options.length);
});
it('el orden de secciones es correcto', () => {
expect(DUPLEX.sectionOrder).toEqual([
SEC_SALON, SEC_DORMITORIO1, SEC_DORMITORIO2, SEC_BANO, SEC_ASEO
]);
});
});
// ============================================================================
// SECCIONES - ATRIBUTOS
// ============================================================================
describe('Secciones - Atributos', () => {
it('el salón tiene suelo, pared, puerta y m²', () => {
const salon = APARTAMENTO.sections[SEC_SALON];
const ids = salon.attrs.map(a => a.id);
expect(ids).toContain(ATTR_SUELO);
expect(ids).toContain(ATTR_PARED);
expect(ids).toContain(ATTR_PUERTA);
});
it('el salón tiene visual config con template de imagen', () => {
const salon = APARTAMENTO.sections[SEC_SALON];
expect(salon.visualConfig.imageUrlTemplate).toBeDefined();
expect(salon.visualConfig.imageUrlTemplate).toContain('{at:suelo}');
expect(salon.visualConfig.imageUrlTemplate).toContain('{at:pared}');
});
it('la cocina tiene encimera, muebles y suelo', () => {
const cocina = APARTAMENTO.sections[SEC_COCINA];
const ids = cocina.attrs.map(a => a.id);
expect(ids).toContain(ATTR_ENCIMERA);
expect(ids).toContain(ATTR_SUELO);
});
it('el baño tiene sanitario y revestimiento', () => {
const bano = APARTAMENTO.sections[SEC_BANO];
const ids = bano.attrs.map(a => a.id);
expect(ids).toContain(ATTR_SANITARIO);
expect(ids).toContain(ATTR_REVEST_BANO);
});
it('los atributos readonly no son visibles en UI', () => {
for (const obj of [APARTAMENTO, DUPLEX]) {
for (const section of Object.values(obj.sections)) {
for (const attr of section.attrs) {
if (attr.display.readonly) {
expect(attr.display.uiVisible).toBeFalsy();
}
}
}
}
});
it('todos los atributos dinámicos tienen defaultValue', () => {
for (const obj of [APARTAMENTO, DUPLEX]) {
for (const section of Object.values(obj.sections)) {
for (const attr of section.attrs) {
if (attr.type === 'dynamic') {
expect((attr as any).defaultValue,
`Atributo ${attr.id} en sección ${section.id} sin defaultValue`
).toBeDefined();
}
}
}
}
});
});
// ============================================================================
// REGLAS - CALIDAD ESTÁNDAR
// ============================================================================
describe('Reglas - Calidad Estándar', () => {
it('prohíbe mármol en baño con calidad estándar', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_ESTANDAR,
[ATTR_REVEST_BANO]: OPT_REVEST_MARMOL // prohibido
};
expect(engine.isValid(rules, state)).toBe(false);
});
it('permite cerámica y microcemento con calidad estándar', () => {
expect(engine.isValueAllowed(
ATTR_REVEST_BANO, OPT_REVEST_CERAMICA, rules,
{ [ATTR_CALIDAD]: OPT_CALIDAD_ESTANDAR }
)).toBe(true);
expect(engine.isValueAllowed(
ATTR_REVEST_BANO, OPT_REVEST_MICROCEMENTO, rules,
{ [ATTR_CALIDAD]: OPT_CALIDAD_ESTANDAR }
)).toBe(true);
});
it('prohíbe sanitarios negros con calidad estándar', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_ESTANDAR,
[ATTR_SANITARIO]: OPT_SANIT_NEGRO
};
expect(engine.isValid(rules, state)).toBe(false);
});
});
// ============================================================================
// REGLAS - CALIDAD PREMIUM
// ============================================================================
describe('Reglas - Calidad Premium', () => {
it('permite mármol con calidad premium', () => {
expect(engine.isValueAllowed(
ATTR_REVEST_BANO, OPT_REVEST_MARMOL, rules,
{ [ATTR_CALIDAD]: OPT_CALIDAD_PREMIUM }
)).toBe(true);
});
it('permite sanitarios negros con calidad premium', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_PREMIUM,
[ATTR_SANITARIO]: OPT_SANIT_NEGRO
};
expect(engine.isValid(rules, state)).toBe(true);
});
});
// ============================================================================
// REGLAS - CALIDAD LUJO
// ============================================================================
describe('Reglas - Calidad Lujo', () => {
it('recomienda mármol en baño (warning)', () => {
const state = { [ATTR_CALIDAD]: OPT_CALIDAD_LUJO };
const results = engine.getAttributeResults(rules, state);
const revest = results.get(ATTR_REVEST_BANO);
expect(revest?.required).toBe(true);
});
it('es válido aunque sin mármol (solo warning, no error)', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_LUJO,
[ATTR_REVEST_BANO]: OPT_REVEST_CERAMICA // no es mármol
};
// La regla de lujo es 'warning', no bloquea
expect(engine.isValid(rules, state)).toBe(true);
});
});
// ============================================================================
// REGLAS - MICROCEMENTO
// ============================================================================
describe('Reglas - Microcemento', () => {
it('con suelo microcemento, solo permite microcemento o cerámica en baño', () => {
const state = { [ATTR_SUELO]: OPT_SUELO_MICROCEMENTO };
expect(engine.isValueAllowed(
ATTR_REVEST_BANO, OPT_REVEST_MICROCEMENTO, rules, state
)).toBe(true);
expect(engine.isValueAllowed(
ATTR_REVEST_BANO, OPT_REVEST_CERAMICA, rules, state
)).toBe(true);
expect(engine.isValueAllowed(
ATTR_REVEST_BANO, OPT_REVEST_MARMOL, rules, state
)).toBe(false);
});
it('con otro suelo, cualquier revestimiento de baño está disponible', () => {
const state = { [ATTR_SUELO]: OPT_SUELO_PARQUET };
// Sin regla microcemento activada, mármol sería válido (si calidad lo permite)
const state2 = {
[ATTR_SUELO] : OPT_SUELO_PARQUET,
[ATTR_CALIDAD]: OPT_CALIDAD_PREMIUM
};
expect(engine.isValueAllowed(
ATTR_REVEST_BANO, OPT_REVEST_MARMOL, rules, state2
)).toBe(true);
});
});
// ============================================================================
// REGLAS - ENCIMERA MADERA
// ============================================================================
describe('Reglas - Encimera de madera', () => {
it('prohíbe sanitarios negros con encimera de madera', () => {
const state = {
[ATTR_ENCIMERA] : OPT_ENCIMERA_MADERA,
[ATTR_SANITARIO]: OPT_SANIT_NEGRO
};
const violations = engine.getViolations(rules, state);
const relevant = violations.filter(v =>
v.rule.affects.includes(ATTR_ENCIMERA)
);
expect(relevant.length).toBeGreaterThan(0);
});
it('permite sanitarios blancos con encimera de madera', () => {
const state = {
[ATTR_ENCIMERA] : OPT_ENCIMERA_MADERA,
[ATTR_SANITARIO]: OPT_SANIT_BLANCO
};
expect(engine.isValueAllowed(
ATTR_SANITARIO, OPT_SANIT_BLANCO, rules, state
)).toBe(true);
});
});
// ============================================================================
// REGLAS - BIDIRECCIONAL (negro ↔ calidad)
// ============================================================================
describe('Reglas - Bidireccional sanitarios negros', () => {
it('seleccionar negro con estándar genera violación', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_ESTANDAR,
[ATTR_SANITARIO]: OPT_SANIT_NEGRO
};
expect(engine.isValid(rules, state)).toBe(false);
});
it('seleccionar estándar con negro ya elegido también genera violación', () => {
// La regla es bidireccional: negro → prohíbe estándar
expect(engine.isValueAllowed(
ATTR_CALIDAD, OPT_CALIDAD_ESTANDAR, rules,
{ [ATTR_SANITARIO]: OPT_SANIT_NEGRO }
)).toBe(false);
});
});
// ============================================================================
// ESCENARIOS COMPLETOS
// ============================================================================
describe('Escenarios completos', () => {
it('Apartamento configuración básica válida', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_ESTANDAR,
[ATTR_SUELO] : OPT_SUELO_CERAMICA,
[ATTR_PARED] : OPT_PARED_BLANCO,
[ATTR_PUERTA] : OPT_PUERTA_LACADA,
[ATTR_ENCIMERA] : OPT_ENCIMERA_GRANITO,
[ATTR_SANITARIO] : OPT_SANIT_BLANCO,
[ATTR_REVEST_BANO]: OPT_REVEST_CERAMICA
};
// 🔍 DEBUG TEMPORAL
const violations = engine.getViolations(rules, state);
console.log('TRIGGERED:', engine.evaluateAll(rules, state)
.filter(r => r.triggered)
.map(r => ({ id: r.rule.id, action: r.action?.type, target: r.action?.targetAttr, values: r.action?.values }))
);
console.log('VIOLATIONS:', violations.map(v => ({ rule: v.rule.id, severity: v.severity })));
const allResults = engine.evaluateAll(rules, state);
const triggered = allResults.filter(r => r.triggered);
console.log('TRIGGERED RULES:', triggered.map(r => ({
id : r.rule.id,
action : r.action
})));
expect(engine.isValid(rules, state)).toBe(true);
expect(engine.getViolations(rules, state)).toHaveLength(0);
});
it('Apartamento configuración premium válida', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_PREMIUM,
[ATTR_SUELO] : OPT_SUELO_PARQUET,
[ATTR_SANITARIO] : OPT_SANIT_NEGRO,
[ATTR_REVEST_BANO]: OPT_REVEST_MARMOL,
[ATTR_ENCIMERA] : OPT_ENCIMERA_GRANITO
};
expect(engine.isValid(rules, state)).toBe(true);
expect(engine.getViolations(rules, state)).toHaveLength(0);
});
it('Dúplex configuración inválida (estándar + negro + mármol)', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_ESTANDAR,
[ATTR_SANITARIO] : OPT_SANIT_NEGRO, // error: requiere premium
[ATTR_REVEST_BANO]: OPT_REVEST_MARMOL // error: no disponible en estándar
};
const violations = engine.getViolations(rules, state);
expect(engine.isValid(rules, state)).toBe(false);
expect(violations.length).toBeGreaterThanOrEqual(2);
expect(violations.every(v => v.severity === 'error')).toBe(true);
});
it('Dúplex configuración de lujo válida', () => {
const state = {
[ATTR_CALIDAD] : OPT_CALIDAD_LUJO,
[ATTR_SUELO] : OPT_SUELO_PARQUET,
[ATTR_SANITARIO] : OPT_SANIT_NEGRO,
[ATTR_REVEST_BANO]: OPT_REVEST_MARMOL,
[ATTR_ENCIMERA] : OPT_ENCIMERA_GRANITO
};
expect(engine.isValid(rules, state)).toBe(true);
});
it('configuración vacía siempre es válida', () => {
expect(engine.isValid(rules, {})).toBe(true);
expect(engine.getViolations(rules, {})).toHaveLength(0);
});
it('las reglas de mayor prioridad se evalúan primero', () => {
const evaluated = engine.evaluateAll(rules, {
[ATTR_CALIDAD]: OPT_CALIDAD_ESTANDAR
});
const priorities = evaluated.map(r => r.rule.priority);
for (let i = 0; i < priorities.length - 1; i++) {
expect(priorities[i]).toBeGreaterThanOrEqual(priorities[i + 1]);
}
});
});

Powered by TurnKey Linux.