parent
f0ba99b7dd
commit
75ec895b66
@ -0,0 +1,262 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* CONFIGURATION STORE — Svelte 5 (Runas)
|
||||
* ============================================================================
|
||||
*
|
||||
* Store de clase reactivo que wrappea ConfigurationEngine.
|
||||
* Se instancia una vez y se distribuye por el árbol de componentes via contexto.
|
||||
*
|
||||
* Uso típico:
|
||||
*
|
||||
* // +layout.svelte — instanciar y proveer
|
||||
* import { setContext } from 'svelte';
|
||||
* import { ConfigurationStore } from '$lib/vice/configuration-store.svelte';
|
||||
*
|
||||
* const store = new ConfigurationStore(catalog, logr);
|
||||
* store.setObject('ob:apartamento');
|
||||
* setContext('configuration', store);
|
||||
*
|
||||
* // ComponenteHijo.svelte — consumir
|
||||
* import { getContext } from 'svelte';
|
||||
* const store = getContext<ConfigurationStore>('configuration');
|
||||
*/
|
||||
|
||||
import type {
|
||||
ConfigurationCatalog,
|
||||
ObjectID,
|
||||
OptionID,
|
||||
SectionID,
|
||||
ViewID,
|
||||
AttrID
|
||||
} from '@/libs/vice/types';
|
||||
import type { Logr } from '@/libs/logr';
|
||||
import { ConfigurationEngine } from '@/libs/vice/engines';
|
||||
import type {
|
||||
ChangeEvent,
|
||||
SelectionOutcome
|
||||
} from '@/libs/vice/engines/configuration';
|
||||
import type {
|
||||
AttributeState,
|
||||
PricingResult
|
||||
} from '@/libs/vice/engines';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Estado reactivo de una vista — URL resuelta + flag de fallback.
|
||||
* Se actualiza automáticamente cuando cambia una opción visual.
|
||||
*/
|
||||
export interface ReactiveView {
|
||||
url : string;
|
||||
isFallback: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION STORE
|
||||
// ============================================================================
|
||||
|
||||
export class ConfigurationStore {
|
||||
// -------------------------------------------------------------------------
|
||||
// Engine interno — no reactivo, no se expone directamente
|
||||
// -------------------------------------------------------------------------
|
||||
readonly #engine: ConfigurationEngine;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Estado reactivo — $state
|
||||
// Svelte trackea estos campos y re-renderiza los componentes que los leen
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** ID del objeto activo */
|
||||
activeObjectId = $state<ObjectID | null>(null);
|
||||
|
||||
/**
|
||||
* Mapa de estados de atributos — AttrID → AttributeState.
|
||||
* Contiene: valor actual, opciones disponibles, required, quantityConfig.
|
||||
* La UI renderiza directamente desde aquí.
|
||||
*/
|
||||
attributeStates = $state<Map<AttrID, AttributeState>>(new Map());
|
||||
|
||||
/**
|
||||
* Resultado de precios — subtotal, tax, total, breakdown.
|
||||
* Se recalcula tras cada selección que affectsPrice.
|
||||
*/
|
||||
price = $state<PricingResult | null>(null);
|
||||
|
||||
/**
|
||||
* Mapa de vistas resueltas — `${sectionId}:${viewId}` → ReactiveView.
|
||||
* Se recalcula tras cada selección que affectsVisual.
|
||||
* La UI suscribe la sección que le interesa.
|
||||
*/
|
||||
views = $state<Map<string, ReactiveView>>(new Map());
|
||||
|
||||
/**
|
||||
* Si el estado actual es válido (sin violaciones de error).
|
||||
*/
|
||||
isValid = $state<boolean>(true);
|
||||
|
||||
/**
|
||||
* Última selección rechazada — para mostrar feedback al usuario.
|
||||
* Se limpia automáticamente en la siguiente selección exitosa.
|
||||
*/
|
||||
lastRejection = $state<{ attrId: AttrID; reason: string } | null>(null);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(catalog: ConfigurationCatalog, logr: Logr) {
|
||||
this.#engine = new ConfigurationEngine(catalog, logr);
|
||||
|
||||
// Suscribirse a eventos del engine — actualizar estado reactivo
|
||||
this.#engine.onChange((event) => this.#handleChange(event));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// API pública
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Activa un objeto del catálogo e inicializa el estado reactivo.
|
||||
*/
|
||||
setObject(objectId: ObjectID): void {
|
||||
this.#engine.setActiveObject(objectId);
|
||||
this.activeObjectId = objectId;
|
||||
this.#syncAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selecciona una opción para un atributo.
|
||||
* Actualiza el estado reactivo si tiene éxito.
|
||||
*/
|
||||
select(attrId: AttrID, optionId: OptionID, quantity?: number): SelectionOutcome {
|
||||
const result = this.#engine.select(attrId, optionId, quantity);
|
||||
|
||||
if (result.success) {
|
||||
this.lastRejection = null;
|
||||
} else {
|
||||
this.lastRejection = { attrId, reason: result.reason ?? '' };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra una vista para seguimiento reactivo.
|
||||
* La UI llama a esto al montar una sección — la vista se mantiene actualizada.
|
||||
* Devuelve función de cleanup para llamar en onDestroy.
|
||||
*/
|
||||
trackView(sectionId: SectionID, viewId: ViewID): () => void {
|
||||
const key = this.#viewKey(sectionId, viewId);
|
||||
this.#resolveView(sectionId, viewId);
|
||||
|
||||
// Cleanup — eliminar la vista del mapa al desmontar la sección
|
||||
return () => {
|
||||
this.views.delete(key);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve el estado de un atributo — usado para acceso directo.
|
||||
* Normalmente la UI lee de `attributeStates` directamente.
|
||||
*/
|
||||
getAttributeState(attrId: AttrID): AttributeState | null {
|
||||
return this.attributeStates.get(attrId) ?? null;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Derived — valores calculados desde el estado reactivo
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Subtotal formateado para mostrar en UI.
|
||||
* Actualización automática cuando cambia `price`.
|
||||
*/
|
||||
get subtotal(): number {
|
||||
return this.price?.subtotal ?? 0;
|
||||
}
|
||||
|
||||
get total(): number {
|
||||
return this.price?.total ?? 0;
|
||||
}
|
||||
|
||||
get taxAmount(): number {
|
||||
return this.price?.taxAmount ?? 0;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Gestión de cambios — privado
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Manejador de eventos del engine.
|
||||
* Actualiza solo el estado reactivo necesario según el tipo de cambio.
|
||||
*/
|
||||
#handleChange(event: ChangeEvent): void {
|
||||
if (event.type === 'object') {
|
||||
// Cambio de objeto — sincronizar todo
|
||||
this.#syncAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cambio de selección — actualizar selectivamente
|
||||
this.#syncAttributeStates();
|
||||
|
||||
if (event.affectsPrice) {
|
||||
this.#syncPrice();
|
||||
}
|
||||
|
||||
if (event.affectsVisual) {
|
||||
this.#syncViews();
|
||||
}
|
||||
|
||||
this.isValid = this.#engine.isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincroniza todo el estado reactivo desde el engine.
|
||||
* Se usa al inicializar o cambiar de objeto.
|
||||
*/
|
||||
#syncAll(): void {
|
||||
this.#syncAttributeStates();
|
||||
this.#syncPrice();
|
||||
this.#syncViews();
|
||||
this.isValid = this.#engine.isValid();
|
||||
}
|
||||
|
||||
#syncAttributeStates(): void {
|
||||
const newStates = this.#engine.getAllAttributeStates();
|
||||
this.attributeStates.clear();
|
||||
for (const [attrId, state] of newStates) {
|
||||
this.attributeStates.set(attrId, state);
|
||||
}
|
||||
}
|
||||
|
||||
#syncPrice(): void {
|
||||
this.price = this.#engine.getPrice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-resuelve todas las vistas registradas.
|
||||
* Solo actualiza las que la UI ha registrado con trackView().
|
||||
*/
|
||||
#syncViews(): void {
|
||||
for (const key of this.views.keys()) {
|
||||
const [sectionId, viewId] = key.split(':') as [SectionID, ViewID];
|
||||
const result = this.#engine.resolveView(sectionId, viewId);
|
||||
this.views.set(key, { url: result.url, isFallback: result.isFallback });
|
||||
}
|
||||
}
|
||||
|
||||
#resolveView(sectionId: SectionID, viewId: ViewID): void {
|
||||
const key = this.#viewKey(sectionId, viewId);
|
||||
const result = this.#engine.resolveView(sectionId, viewId);
|
||||
this.views.set(key, { url: result.url, isFallback: result.isFallback });
|
||||
}
|
||||
|
||||
#viewKey(sectionId: SectionID, viewId: ViewID): string {
|
||||
return `${sectionId}:${viewId}`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,333 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* CONFIGURATION STORE — TESTS (Svelte 5 / Runas)
|
||||
* ============================================================================
|
||||
*
|
||||
* Los tests instancian el store directamente.
|
||||
* $state fuera del compilador de Svelte se comporta como propiedad normal.
|
||||
* La lógica del store se verifica sin necesitar DOM ni contexto de Svelte.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ConfigurationStore } from '../stores/configuration.svelte.ts';
|
||||
import type { Logr } from '@/libs/logr';
|
||||
import {
|
||||
CATALOGO_VIVIENDAS,
|
||||
IDS_TEST,
|
||||
} from '@/libs/vice/fixtures';
|
||||
|
||||
const {
|
||||
OBJ_APARTAMENTO,
|
||||
OBJ_DUPLEX,
|
||||
ATTR_CALIDAD,
|
||||
ATTR_SUELO,
|
||||
ATTR_PARED,
|
||||
ATTR_REVEST_BANO,
|
||||
ATTR_SANITARIO,
|
||||
ATTR_ARMARIO_PUERTAS,
|
||||
OPT_CALIDAD_ESTANDAR,
|
||||
OPT_CALIDAD_PREMIUM,
|
||||
OPT_CALIDAD_LUJO,
|
||||
OPT_SUELO_PARQUET,
|
||||
OPT_SUELO_CERAMICA,
|
||||
OPT_REVEST_CERAMICA,
|
||||
OPT_REVEST_MARMOL,
|
||||
OPT_SANIT_NEGRO,
|
||||
OPT_ARMARIO_LACADO,
|
||||
OPT_ARMARIO_MADERA,
|
||||
SEC_SALON,
|
||||
SEC_BANO,
|
||||
VIEW_FRONT,
|
||||
VIEW_360,
|
||||
} = IDS_TEST;
|
||||
|
||||
// ============================================================================
|
||||
// SETUP
|
||||
// ============================================================================
|
||||
|
||||
function makeLogr(): Logr {
|
||||
return {
|
||||
debug : vi.fn(),
|
||||
info : vi.fn(),
|
||||
warn : vi.fn(),
|
||||
error : vi.fn(),
|
||||
getLogs : vi.fn(),
|
||||
clear : vi.fn(),
|
||||
serialize: vi.fn(),
|
||||
setLevel : vi.fn(),
|
||||
setMaxLogs: vi.fn(),
|
||||
} as unknown as Logr;
|
||||
}
|
||||
|
||||
function makeStore() {
|
||||
return new ConfigurationStore(CATALOGO_VIVIENDAS, makeLogr());
|
||||
}
|
||||
|
||||
let store: ConfigurationStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = makeStore();
|
||||
store.setObject(OBJ_APARTAMENTO);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// setObject
|
||||
// ============================================================================
|
||||
|
||||
describe('setObject', () => {
|
||||
|
||||
it('actualiza activeObjectId', () => {
|
||||
expect(store.activeObjectId).toBe(OBJ_APARTAMENTO);
|
||||
});
|
||||
|
||||
it('inicializa attributeStates', () => {
|
||||
expect(store.attributeStates.size).toBeGreaterThan(0);
|
||||
expect(store.attributeStates.has(ATTR_CALIDAD)).toBe(true);
|
||||
expect(store.attributeStates.has(ATTR_SUELO)).toBe(true);
|
||||
});
|
||||
|
||||
it('inicializa price', () => {
|
||||
expect(store.price).not.toBeNull();
|
||||
expect(typeof store.price?.subtotal).toBe('number');
|
||||
});
|
||||
|
||||
it('cambia activeObjectId al cambiar de objeto', () => {
|
||||
store.setObject(OBJ_DUPLEX);
|
||||
expect(store.activeObjectId).toBe(OBJ_DUPLEX);
|
||||
});
|
||||
|
||||
it('preserva el estado al volver al objeto anterior', () => {
|
||||
store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
store.setObject(OBJ_DUPLEX);
|
||||
store.setObject(OBJ_APARTAMENTO);
|
||||
const attrState = store.attributeStates.get(ATTR_SUELO);
|
||||
expect((attrState?.value as any).optionId).toBe(OPT_SUELO_PARQUET);
|
||||
});
|
||||
|
||||
it('attributeStates del dúplex son independientes del apartamento', () => {
|
||||
store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
store.setObject(OBJ_DUPLEX);
|
||||
const attrState = store.attributeStates.get(ATTR_SUELO);
|
||||
expect((attrState?.value as any).optionId).not.toBe(OPT_SUELO_PARQUET);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// select
|
||||
// ============================================================================
|
||||
|
||||
describe('select', () => {
|
||||
|
||||
it('acepta selección válida y devuelve success: true', () => {
|
||||
const result = store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('actualiza attributeStates tras selección válida', () => {
|
||||
store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
const attrState = store.attributeStates.get(ATTR_SUELO);
|
||||
expect((attrState?.value as any).optionId).toBe(OPT_SUELO_PARQUET);
|
||||
});
|
||||
|
||||
it('rechaza selección prohibida y devuelve success: false', () => {
|
||||
const result = store.select(ATTR_REVEST_BANO, OPT_REVEST_MARMOL);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('establece lastRejection al rechazar', () => {
|
||||
store.select(ATTR_REVEST_BANO, OPT_REVEST_MARMOL);
|
||||
expect(store.lastRejection).not.toBeNull();
|
||||
expect(store.lastRejection?.attrId).toBe(ATTR_REVEST_BANO);
|
||||
expect(store.lastRejection?.reason).toBeTruthy();
|
||||
});
|
||||
|
||||
it('limpia lastRejection tras selección exitosa', () => {
|
||||
store.select(ATTR_REVEST_BANO, OPT_REVEST_MARMOL);
|
||||
store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
expect(store.lastRejection).toBeNull();
|
||||
});
|
||||
|
||||
it('attributeStates no cambia tras selección rechazada', () => {
|
||||
const before = store.attributeStates.get(ATTR_REVEST_BANO)?.value;
|
||||
store.select(ATTR_REVEST_BANO, OPT_REVEST_MARMOL);
|
||||
const after = store.attributeStates.get(ATTR_REVEST_BANO)?.value;
|
||||
expect(after).toEqual(before);
|
||||
});
|
||||
|
||||
it('acepta selección con cantidad', () => {
|
||||
const result = store.select(ATTR_ARMARIO_PUERTAS, OPT_ARMARIO_LACADO, 3);
|
||||
expect(result.success).toBe(true);
|
||||
const attrState = store.attributeStates.get(ATTR_ARMARIO_PUERTAS);
|
||||
expect((attrState?.value as any).quantity).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// price
|
||||
// ============================================================================
|
||||
|
||||
describe('price', () => {
|
||||
|
||||
it('subtotal es un número', () => {
|
||||
expect(typeof store.subtotal).toBe('number');
|
||||
expect(store.subtotal).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('total es un número', () => {
|
||||
expect(typeof store.total).toBe('number');
|
||||
});
|
||||
|
||||
it('price se actualiza tras selección que affectsPrice', () => {
|
||||
const before = store.subtotal;
|
||||
store.select(ATTR_CALIDAD, OPT_CALIDAD_PREMIUM);
|
||||
expect(store.subtotal).toBeGreaterThan(before);
|
||||
});
|
||||
|
||||
it('price no cambia tras selección que no affectsPrice', () => {
|
||||
// ATTR_PARED tiene affectsPrice: false (precio 0 en todas sus opciones pero
|
||||
// el flag affectsPrice es true en el fixture — verificamos que el subtotal no cambia)
|
||||
const before = store.subtotal;
|
||||
store.select(ATTR_PARED, IDS_TEST.OPT_PARED_GRIS);
|
||||
// pared gris tiene baseAmount: 0 igual que blanco → subtotal igual
|
||||
expect(store.subtotal).toBe(before);
|
||||
});
|
||||
|
||||
it('price con armario 3 puertas lacadas suma 900', () => {
|
||||
const base = store.subtotal;
|
||||
store.select(ATTR_ARMARIO_PUERTAS, OPT_ARMARIO_LACADO, 3);
|
||||
// 2 puertas → 700, 3 puertas → 900 → diferencia 200
|
||||
expect(store.subtotal - base).toBe(200);
|
||||
});
|
||||
|
||||
it('getters subtotal/total/taxAmount son consistentes', () => {
|
||||
expect(store.total).toBeGreaterThanOrEqual(store.subtotal);
|
||||
expect(store.taxAmount).toBeGreaterThanOrEqual(0);
|
||||
expect(store.subtotal + store.taxAmount).toBeCloseTo(store.total, 2);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// attributeStates — disponibilidad de opciones
|
||||
// ============================================================================
|
||||
|
||||
describe('attributeStates — disponibilidad', () => {
|
||||
|
||||
it('mármol no disponible con calidad estándar', () => {
|
||||
const s = store.attributeStates.get(ATTR_REVEST_BANO);
|
||||
const marmol = s?.options.find(o => o.optionId === OPT_REVEST_MARMOL);
|
||||
expect(marmol?.available).toBe(false);
|
||||
});
|
||||
|
||||
it('mármol disponible tras cambiar a premium', () => {
|
||||
store.select(ATTR_CALIDAD, OPT_CALIDAD_PREMIUM);
|
||||
const s = store.attributeStates.get(ATTR_REVEST_BANO);
|
||||
const marmol = s?.options.find(o => o.optionId === OPT_REVEST_MARMOL);
|
||||
expect(marmol?.available).toBe(true);
|
||||
});
|
||||
|
||||
it('opción no disponible tiene reason para tooltip', () => {
|
||||
const s = store.attributeStates.get(ATTR_REVEST_BANO);
|
||||
const marmol = s?.options.find(o => o.optionId === OPT_REVEST_MARMOL);
|
||||
expect(marmol?.reason).toBeTruthy();
|
||||
});
|
||||
|
||||
it('sanitario negro no disponible con calidad estándar', () => {
|
||||
const s = store.attributeStates.get(ATTR_SANITARIO);
|
||||
const negro = s?.options.find(o => o.optionId === OPT_SANIT_NEGRO);
|
||||
expect(negro?.available).toBe(false);
|
||||
});
|
||||
|
||||
it('quantityConfig disponible para opción cuantificable', () => {
|
||||
const s = store.attributeStates.get(ATTR_ARMARIO_PUERTAS);
|
||||
const lacado = s?.options.find(o => o.optionId === OPT_ARMARIO_LACADO);
|
||||
expect(lacado?.quantityConfig).toBeDefined();
|
||||
expect(lacado?.quantityConfig?.min).toBe(2);
|
||||
expect(lacado?.quantityConfig?.max).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// isValid
|
||||
// ============================================================================
|
||||
|
||||
describe('isValid', () => {
|
||||
|
||||
it('estado inicial es válido', () => {
|
||||
expect(store.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('sigue válido tras selección permitida', () => {
|
||||
store.select(ATTR_CALIDAD, OPT_CALIDAD_PREMIUM);
|
||||
expect(store.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// trackView
|
||||
// ============================================================================
|
||||
|
||||
describe('trackView', () => {
|
||||
|
||||
it('registra la vista y la resuelve', () => {
|
||||
store.trackView(SEC_SALON, VIEW_FRONT);
|
||||
const key = `${SEC_SALON}:${VIEW_FRONT}`;
|
||||
const view = store.views.get(key);
|
||||
expect(view).toBeDefined();
|
||||
expect(typeof view?.url).toBe('string');
|
||||
expect(view?.url.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('vista no es fallback para sección válida', () => {
|
||||
store.trackView(SEC_SALON, VIEW_FRONT);
|
||||
const view = store.views.get(`${SEC_SALON}:${VIEW_FRONT}`);
|
||||
expect(view?.isFallback).toBe(false);
|
||||
});
|
||||
|
||||
it('la URL cambia al cambiar una opción visual', () => {
|
||||
store.trackView(SEC_SALON, VIEW_FRONT);
|
||||
const before = store.views.get(`${SEC_SALON}:${VIEW_FRONT}`)?.url;
|
||||
store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
const after = store.views.get(`${SEC_SALON}:${VIEW_FRONT}`)?.url;
|
||||
console.log('before:', before);
|
||||
console.log('after:', after);
|
||||
expect(after).not.toBe(before);
|
||||
});
|
||||
|
||||
it('vistas no registradas no se actualizan', () => {
|
||||
// SEC_BANO no está registrada — no debe aparecer en views
|
||||
store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
const banoKey = `${SEC_BANO}:${VIEW_FRONT}`;
|
||||
expect(store.views.has(banoKey)).toBe(false);
|
||||
});
|
||||
|
||||
it('cleanup elimina la vista del mapa', () => {
|
||||
const cleanup = store.trackView(SEC_SALON, VIEW_FRONT);
|
||||
cleanup();
|
||||
expect(store.views.has(`${SEC_SALON}:${VIEW_FRONT}`)).toBe(false);
|
||||
});
|
||||
|
||||
it('múltiples vistas registradas se actualizan independientemente', () => {
|
||||
store.trackView(SEC_SALON, VIEW_FRONT);
|
||||
store.trackView(SEC_BANO, VIEW_FRONT);
|
||||
store.select(ATTR_SUELO, OPT_SUELO_PARQUET);
|
||||
expect(store.views.has(`${SEC_SALON}:${VIEW_FRONT}`)).toBe(true);
|
||||
expect(store.views.has(`${SEC_BANO}:${VIEW_FRONT}`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// getAttributeState
|
||||
// ============================================================================
|
||||
|
||||
describe('getAttributeState', () => {
|
||||
|
||||
it('devuelve el estado de un atributo', () => {
|
||||
const s = store.getAttributeState(ATTR_SUELO);
|
||||
expect(s).not.toBeNull();
|
||||
expect(s?.options.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('devuelve null para atributo inexistente', () => {
|
||||
expect(store.getAttributeState('at:noexiste' as any)).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue