vitest
This commit is contained in:
1711
package-lock.json
generated
1711
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,9 @@
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"migrate:up": "dbmate up",
|
||||
"migrate:down": "dbmate down",
|
||||
"migrate:redo": "dbmate rollback && dbmate up",
|
||||
@@ -27,7 +30,9 @@
|
||||
"zod": "^4.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"dbmate": "^2.0.0",
|
||||
"nodemon": "^3.0.3"
|
||||
"nodemon": "^3.0.3",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
565
src/modules/3-turn-engine/fsm.test.js
Normal file
565
src/modules/3-turn-engine/fsm.test.js
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Tests para fsm.js
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
ConversationState,
|
||||
ALL_STATES,
|
||||
INTENTS_BY_STATE,
|
||||
shouldReturnToCart,
|
||||
hasCartItems,
|
||||
hasPendingItems,
|
||||
hasReadyPendingItems,
|
||||
hasShippingInfo,
|
||||
hasPaymentInfo,
|
||||
isPaid,
|
||||
deriveNextState,
|
||||
validateTransition,
|
||||
safeNextState,
|
||||
} from './fsm.js';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Constants
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('ConversationState', () => {
|
||||
it('tiene todos los estados definidos', () => {
|
||||
expect(ConversationState.IDLE).toBe('IDLE');
|
||||
expect(ConversationState.CART).toBe('CART');
|
||||
expect(ConversationState.SHIPPING).toBe('SHIPPING');
|
||||
expect(ConversationState.PAYMENT).toBe('PAYMENT');
|
||||
expect(ConversationState.WAITING_WEBHOOKS).toBe('WAITING_WEBHOOKS');
|
||||
expect(ConversationState.AWAITING_HUMAN).toBe('AWAITING_HUMAN');
|
||||
});
|
||||
|
||||
it('ALL_STATES contiene todos', () => {
|
||||
expect(ALL_STATES).toContain('IDLE');
|
||||
expect(ALL_STATES).toContain('CART');
|
||||
expect(ALL_STATES).toContain('SHIPPING');
|
||||
expect(ALL_STATES).toContain('PAYMENT');
|
||||
expect(ALL_STATES).toContain('WAITING_WEBHOOKS');
|
||||
expect(ALL_STATES).toContain('AWAITING_HUMAN');
|
||||
expect(ALL_STATES).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('INTENTS_BY_STATE define intents para cada estado', () => {
|
||||
expect(INTENTS_BY_STATE[ConversationState.IDLE]).toContain('greeting');
|
||||
expect(INTENTS_BY_STATE[ConversationState.CART]).toContain('add_to_cart');
|
||||
expect(INTENTS_BY_STATE[ConversationState.SHIPPING]).toContain('provide_address');
|
||||
expect(INTENTS_BY_STATE[ConversationState.PAYMENT]).toContain('select_payment');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// hasCartItems
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('hasCartItems', () => {
|
||||
it('retorna true si cart tiene items', () => {
|
||||
const order = { cart: [{ woo_id: 1, qty: 1 }] };
|
||||
expect(hasCartItems(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna false si cart está vacío', () => {
|
||||
const order = { cart: [] };
|
||||
expect(hasCartItems(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si cart es undefined', () => {
|
||||
const order = {};
|
||||
expect(hasCartItems(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si order es null', () => {
|
||||
expect(hasCartItems(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si order es undefined', () => {
|
||||
expect(hasCartItems(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// hasPendingItems
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('hasPendingItems', () => {
|
||||
it('retorna true si hay NEEDS_TYPE', () => {
|
||||
const order = { pending: [{ status: 'NEEDS_TYPE' }] };
|
||||
expect(hasPendingItems(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna true si hay NEEDS_QUANTITY', () => {
|
||||
const order = { pending: [{ status: 'NEEDS_QUANTITY' }] };
|
||||
expect(hasPendingItems(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna false si solo hay READY', () => {
|
||||
const order = { pending: [{ status: 'READY' }] };
|
||||
expect(hasPendingItems(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si pending está vacío', () => {
|
||||
const order = { pending: [] };
|
||||
expect(hasPendingItems(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si order es null', () => {
|
||||
expect(hasPendingItems(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('detecta entre múltiples items', () => {
|
||||
const order = {
|
||||
pending: [
|
||||
{ status: 'READY' },
|
||||
{ status: 'NEEDS_TYPE' },
|
||||
]
|
||||
};
|
||||
expect(hasPendingItems(order)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// hasReadyPendingItems
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('hasReadyPendingItems', () => {
|
||||
it('retorna true si hay READY', () => {
|
||||
const order = { pending: [{ status: 'READY' }] };
|
||||
expect(hasReadyPendingItems(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna false si no hay READY', () => {
|
||||
const order = { pending: [{ status: 'NEEDS_TYPE' }] };
|
||||
expect(hasReadyPendingItems(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si pending vacío', () => {
|
||||
const order = { pending: [] };
|
||||
expect(hasReadyPendingItems(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si order es null', () => {
|
||||
expect(hasReadyPendingItems(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// hasShippingInfo
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('hasShippingInfo', () => {
|
||||
it('retorna true para pickup (no necesita dirección)', () => {
|
||||
const order = { is_delivery: false };
|
||||
expect(hasShippingInfo(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna true para delivery con dirección', () => {
|
||||
const order = { is_delivery: true, shipping_address: 'Calle Falsa 123' };
|
||||
expect(hasShippingInfo(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna false para delivery sin dirección', () => {
|
||||
const order = { is_delivery: true, shipping_address: null };
|
||||
expect(hasShippingInfo(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si is_delivery es null', () => {
|
||||
const order = { is_delivery: null };
|
||||
expect(hasShippingInfo(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false para order vacío', () => {
|
||||
expect(hasShippingInfo({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// hasPaymentInfo
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('hasPaymentInfo', () => {
|
||||
it('retorna true para cash', () => {
|
||||
const order = { payment_type: 'cash' };
|
||||
expect(hasPaymentInfo(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna true para link', () => {
|
||||
const order = { payment_type: 'link' };
|
||||
expect(hasPaymentInfo(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna false para null', () => {
|
||||
const order = { payment_type: null };
|
||||
expect(hasPaymentInfo(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false para undefined', () => {
|
||||
const order = {};
|
||||
expect(hasPaymentInfo(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false para otros valores', () => {
|
||||
const order = { payment_type: 'bitcoin' };
|
||||
expect(hasPaymentInfo(order)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// isPaid
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('isPaid', () => {
|
||||
it('retorna true si is_paid es true', () => {
|
||||
const order = { is_paid: true };
|
||||
expect(isPaid(order)).toBe(true);
|
||||
});
|
||||
|
||||
it('retorna false si is_paid es false', () => {
|
||||
const order = { is_paid: false };
|
||||
expect(isPaid(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si is_paid es undefined', () => {
|
||||
const order = {};
|
||||
expect(isPaid(order)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false si order es null', () => {
|
||||
expect(isPaid(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// shouldReturnToCart
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('shouldReturnToCart', () => {
|
||||
describe('no redirige si ya está en CART o IDLE', () => {
|
||||
it('retorna false en CART', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: { product_query: 'provoleta' } };
|
||||
expect(shouldReturnToCart(ConversationState.CART, nlu)).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna false en IDLE', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: { product_query: 'provoleta' } };
|
||||
expect(shouldReturnToCart(ConversationState.IDLE, nlu)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirige desde otros estados', () => {
|
||||
it('redirige add_to_cart desde SHIPPING', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: { product_query: 'provoleta' } };
|
||||
expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(true);
|
||||
});
|
||||
|
||||
it('redirige add_to_cart desde PAYMENT', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: { product_query: 'vacío' } };
|
||||
expect(shouldReturnToCart(ConversationState.PAYMENT, nlu)).toBe(true);
|
||||
});
|
||||
|
||||
it('redirige browse desde SHIPPING', () => {
|
||||
const nlu = { intent: 'browse', entities: { product_query: 'carnes' } };
|
||||
expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no redirige números solos en checkout', () => {
|
||||
it('no redirige "1" en SHIPPING', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: {} };
|
||||
expect(shouldReturnToCart(ConversationState.SHIPPING, nlu, '1')).toBe(false);
|
||||
});
|
||||
|
||||
it('no redirige "2" en PAYMENT', () => {
|
||||
const nlu = { intent: 'other', entities: {} };
|
||||
expect(shouldReturnToCart(ConversationState.PAYMENT, nlu, '2')).toBe(false);
|
||||
});
|
||||
|
||||
it('no redirige "1.5" en SHIPPING', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: {} };
|
||||
expect(shouldReturnToCart(ConversationState.SHIPPING, nlu, '1.5')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requiere producto real', () => {
|
||||
it('no redirige sin product_query', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: {} };
|
||||
expect(shouldReturnToCart(ConversationState.SHIPPING, nlu, 'algo')).toBe(false);
|
||||
});
|
||||
|
||||
it('no redirige con product_query muy corto', () => {
|
||||
const nlu = { intent: 'add_to_cart', entities: { product_query: 'ab' } };
|
||||
expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(false);
|
||||
});
|
||||
|
||||
it('redirige con items array', () => {
|
||||
const nlu = {
|
||||
intent: 'add_to_cart',
|
||||
entities: { items: [{ product_query: 'provoleta' }] }
|
||||
};
|
||||
expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// deriveNextState
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('deriveNextState', () => {
|
||||
describe('return_to_cart signal', () => {
|
||||
it('fuerza CART si return_to_cart', () => {
|
||||
const result = deriveNextState(
|
||||
ConversationState.PAYMENT,
|
||||
{},
|
||||
{ return_to_cart: true }
|
||||
);
|
||||
expect(result).toBe(ConversationState.CART);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagado', () => {
|
||||
it('va a IDLE si está pagado', () => {
|
||||
const order = { is_paid: true };
|
||||
const result = deriveNextState(ConversationState.WAITING_WEBHOOKS, order, {});
|
||||
expect(result).toBe(ConversationState.IDLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('esperando pago', () => {
|
||||
it('va a WAITING_WEBHOOKS si tiene woo_order_id', () => {
|
||||
const order = { woo_order_id: 123, is_paid: false };
|
||||
const result = deriveNextState(ConversationState.PAYMENT, order, {});
|
||||
expect(result).toBe(ConversationState.WAITING_WEBHOOKS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IDLE -> CART', () => {
|
||||
it('va a CART si hay cart items', () => {
|
||||
const order = { cart: [{ woo_id: 1 }], pending: [] };
|
||||
const result = deriveNextState(ConversationState.IDLE, order, {});
|
||||
expect(result).toBe(ConversationState.CART);
|
||||
});
|
||||
|
||||
it('va a CART si hay pending items', () => {
|
||||
const order = { cart: [], pending: [{ status: 'NEEDS_TYPE' }] };
|
||||
const result = deriveNextState(ConversationState.IDLE, order, {});
|
||||
expect(result).toBe(ConversationState.CART);
|
||||
});
|
||||
|
||||
it('queda en IDLE si vacío', () => {
|
||||
const order = { cart: [], pending: [] };
|
||||
const result = deriveNextState(ConversationState.IDLE, order, {});
|
||||
expect(result).toBe(ConversationState.IDLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CART -> SHIPPING', () => {
|
||||
it('queda en CART si hay pending items', () => {
|
||||
const order = { cart: [{ woo_id: 1 }], pending: [{ status: 'NEEDS_TYPE' }] };
|
||||
const result = deriveNextState(ConversationState.CART, order, { confirm_order: true });
|
||||
expect(result).toBe(ConversationState.CART);
|
||||
});
|
||||
|
||||
it('va a SHIPPING con confirm_order y cart items', () => {
|
||||
const order = { cart: [{ woo_id: 1 }], pending: [] };
|
||||
const result = deriveNextState(ConversationState.CART, order, { confirm_order: true });
|
||||
expect(result).toBe(ConversationState.SHIPPING);
|
||||
});
|
||||
|
||||
it('queda en CART sin confirm_order', () => {
|
||||
const order = { cart: [{ woo_id: 1 }], pending: [] };
|
||||
const result = deriveNextState(ConversationState.CART, order, {});
|
||||
expect(result).toBe(ConversationState.CART);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SHIPPING -> PAYMENT', () => {
|
||||
it('va a PAYMENT con shipping info (pickup)', () => {
|
||||
const order = { is_delivery: false };
|
||||
const result = deriveNextState(ConversationState.SHIPPING, order, {});
|
||||
expect(result).toBe(ConversationState.PAYMENT);
|
||||
});
|
||||
|
||||
it('va a PAYMENT con shipping info (delivery + address)', () => {
|
||||
const order = { is_delivery: true, shipping_address: 'Calle 123' };
|
||||
const result = deriveNextState(ConversationState.SHIPPING, order, {});
|
||||
expect(result).toBe(ConversationState.PAYMENT);
|
||||
});
|
||||
|
||||
it('queda en SHIPPING sin info completa', () => {
|
||||
const order = { is_delivery: true, shipping_address: null };
|
||||
const result = deriveNextState(ConversationState.SHIPPING, order, {});
|
||||
expect(result).toBe(ConversationState.SHIPPING);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PAYMENT -> WAITING_WEBHOOKS', () => {
|
||||
it('va a WAITING con payment_selected', () => {
|
||||
const order = {};
|
||||
const result = deriveNextState(
|
||||
ConversationState.PAYMENT,
|
||||
order,
|
||||
{ payment_selected: true }
|
||||
);
|
||||
expect(result).toBe(ConversationState.WAITING_WEBHOOKS);
|
||||
});
|
||||
|
||||
it('va a WAITING si ya tiene payment_type', () => {
|
||||
const order = { payment_type: 'cash' };
|
||||
const result = deriveNextState(ConversationState.PAYMENT, order, {});
|
||||
expect(result).toBe(ConversationState.WAITING_WEBHOOKS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WAITING_WEBHOOKS', () => {
|
||||
it('va a IDLE si está pagado', () => {
|
||||
const order = { is_paid: true };
|
||||
const result = deriveNextState(ConversationState.WAITING_WEBHOOKS, order, {});
|
||||
expect(result).toBe(ConversationState.IDLE);
|
||||
});
|
||||
|
||||
it('queda en WAITING si no está pagado', () => {
|
||||
const order = { is_paid: false };
|
||||
const result = deriveNextState(ConversationState.WAITING_WEBHOOKS, order, {});
|
||||
expect(result).toBe(ConversationState.WAITING_WEBHOOKS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
it('retorna IDLE si no hay estado previo', () => {
|
||||
const result = deriveNextState(null, {}, {});
|
||||
expect(result).toBe(ConversationState.IDLE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// validateTransition
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('validateTransition', () => {
|
||||
describe('transiciones válidas', () => {
|
||||
it('IDLE -> IDLE es válido', () => {
|
||||
const result = validateTransition(ConversationState.IDLE, ConversationState.IDLE);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('IDLE -> CART es válido', () => {
|
||||
const result = validateTransition(ConversationState.IDLE, ConversationState.CART);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('CART -> SHIPPING es válido', () => {
|
||||
const result = validateTransition(ConversationState.CART, ConversationState.SHIPPING);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('SHIPPING -> PAYMENT es válido', () => {
|
||||
const result = validateTransition(ConversationState.SHIPPING, ConversationState.PAYMENT);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('PAYMENT -> WAITING_WEBHOOKS es válido', () => {
|
||||
const result = validateTransition(ConversationState.PAYMENT, ConversationState.WAITING_WEBHOOKS);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('SHIPPING -> CART (volver) es válido', () => {
|
||||
const result = validateTransition(ConversationState.SHIPPING, ConversationState.CART);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transiciones inválidas', () => {
|
||||
it('IDLE -> PAYMENT es inválido', () => {
|
||||
const result = validateTransition(ConversationState.IDLE, ConversationState.PAYMENT);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('invalid_transition');
|
||||
});
|
||||
|
||||
it('CART -> WAITING_WEBHOOKS es inválido', () => {
|
||||
const result = validateTransition(ConversationState.CART, ConversationState.WAITING_WEBHOOKS);
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('estados desconocidos', () => {
|
||||
it('estado previo desconocido', () => {
|
||||
const result = validateTransition('UNKNOWN', ConversationState.CART);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('unknown_prev_state');
|
||||
});
|
||||
|
||||
it('estado siguiente desconocido', () => {
|
||||
const result = validateTransition(ConversationState.IDLE, 'UNKNOWN');
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('unknown_next_state');
|
||||
});
|
||||
});
|
||||
|
||||
describe('maneja null/undefined', () => {
|
||||
it('prevState null se trata como IDLE', () => {
|
||||
const result = validateTransition(null, ConversationState.CART);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('nextState null se trata como IDLE', () => {
|
||||
const result = validateTransition(ConversationState.IDLE, null);
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// safeNextState
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('safeNextState', () => {
|
||||
it('retorna estado derivado si transición válida', () => {
|
||||
const order = { cart: [{ woo_id: 1 }], pending: [] };
|
||||
const result = safeNextState(ConversationState.CART, order, { confirm_order: true });
|
||||
|
||||
expect(result.next_state).toBe(ConversationState.SHIPPING);
|
||||
expect(result.validation.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('fuerza CART si transición inválida', () => {
|
||||
// Forzar una situación donde deriveNextState retornaría un estado inválido
|
||||
// Esto es difícil de provocar porque deriveNextState ya es bastante seguro
|
||||
// Pero podemos verificar que la lógica de fallback existe
|
||||
const order = {};
|
||||
const result = safeNextState(ConversationState.IDLE, order, {});
|
||||
|
||||
// Debería quedarse en IDLE (transición válida)
|
||||
expect(result.next_state).toBe(ConversationState.IDLE);
|
||||
expect(result.validation.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('incluye validation en resultado', () => {
|
||||
const order = { is_delivery: false };
|
||||
const result = safeNextState(ConversationState.SHIPPING, order, {});
|
||||
|
||||
expect(result).toHaveProperty('next_state');
|
||||
expect(result).toHaveProperty('validation');
|
||||
expect(result.validation).toHaveProperty('ok');
|
||||
});
|
||||
|
||||
it('maneja transition IDLE -> CART -> SHIPPING flow', () => {
|
||||
// Paso 1: IDLE con cart items -> CART
|
||||
let result = safeNextState(ConversationState.IDLE, { cart: [{ woo_id: 1 }], pending: [] }, {});
|
||||
expect(result.next_state).toBe(ConversationState.CART);
|
||||
|
||||
// Paso 2: CART con confirm -> SHIPPING
|
||||
result = safeNextState(ConversationState.CART, { cart: [{ woo_id: 1 }], pending: [] }, { confirm_order: true });
|
||||
expect(result.next_state).toBe(ConversationState.SHIPPING);
|
||||
|
||||
// Paso 3: SHIPPING con pickup -> PAYMENT
|
||||
result = safeNextState(ConversationState.SHIPPING, { is_delivery: false }, {});
|
||||
expect(result.next_state).toBe(ConversationState.PAYMENT);
|
||||
|
||||
// Paso 4: PAYMENT con payment_selected -> WAITING
|
||||
result = safeNextState(ConversationState.PAYMENT, {}, { payment_selected: true });
|
||||
expect(result.next_state).toBe(ConversationState.WAITING_WEBHOOKS);
|
||||
});
|
||||
});
|
||||
748
src/modules/3-turn-engine/orderModel.test.js
Normal file
748
src/modules/3-turn-engine/orderModel.test.js
Normal file
@@ -0,0 +1,748 @@
|
||||
/**
|
||||
* Tests para orderModel.js
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
PendingStatus,
|
||||
createEmptyOrder,
|
||||
createCartItem,
|
||||
createPendingItem,
|
||||
moveReadyToCart,
|
||||
getNextPendingItem,
|
||||
updatePendingItem,
|
||||
addPendingItem,
|
||||
removeCartItem,
|
||||
updateCartItemQuantity,
|
||||
migrateOldContext,
|
||||
formatCartForDisplay,
|
||||
formatOptionsForDisplay,
|
||||
} from './orderModel.js';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// createEmptyOrder
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('createEmptyOrder', () => {
|
||||
it('crea orden con estructura correcta', () => {
|
||||
const order = createEmptyOrder();
|
||||
expect(order).toHaveProperty('cart');
|
||||
expect(order).toHaveProperty('pending');
|
||||
expect(order).toHaveProperty('payment_type');
|
||||
expect(order).toHaveProperty('is_delivery');
|
||||
expect(order).toHaveProperty('shipping_address');
|
||||
expect(order).toHaveProperty('woo_order_id');
|
||||
expect(order).toHaveProperty('is_paid');
|
||||
});
|
||||
|
||||
it('inicializa arrays vacíos', () => {
|
||||
const order = createEmptyOrder();
|
||||
expect(order.cart).toEqual([]);
|
||||
expect(order.pending).toEqual([]);
|
||||
});
|
||||
|
||||
it('inicializa valores null/false', () => {
|
||||
const order = createEmptyOrder();
|
||||
expect(order.payment_type).toBeNull();
|
||||
expect(order.is_delivery).toBeNull();
|
||||
expect(order.shipping_address).toBeNull();
|
||||
expect(order.woo_order_id).toBeNull();
|
||||
expect(order.is_paid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// createCartItem
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('createCartItem', () => {
|
||||
it('crea item con todos los campos', () => {
|
||||
const item = createCartItem({
|
||||
woo_id: 123,
|
||||
qty: 2,
|
||||
unit: 'kg',
|
||||
name: 'Vacío',
|
||||
price: 5000,
|
||||
});
|
||||
expect(item.woo_id).toBe(123);
|
||||
expect(item.qty).toBe(2);
|
||||
expect(item.unit).toBe('kg');
|
||||
expect(item.name).toBe('Vacío');
|
||||
expect(item.price).toBe(5000);
|
||||
});
|
||||
|
||||
it('usa defaults correctos', () => {
|
||||
const item = createCartItem({ woo_id: 123 });
|
||||
expect(item.qty).toBe(1);
|
||||
expect(item.unit).toBe('unit');
|
||||
expect(item.name).toBeNull();
|
||||
expect(item.price).toBeNull();
|
||||
});
|
||||
|
||||
it('convierte qty a número', () => {
|
||||
const item = createCartItem({ woo_id: 123, qty: '3' });
|
||||
expect(item.qty).toBe(3);
|
||||
});
|
||||
|
||||
it('usa 1 si qty es inválido', () => {
|
||||
const item = createCartItem({ woo_id: 123, qty: 'abc' });
|
||||
expect(item.qty).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// createPendingItem
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('createPendingItem', () => {
|
||||
it('genera id automático si no se proporciona', () => {
|
||||
const item = createPendingItem({ query: 'provoleta' });
|
||||
expect(item.id).toBeDefined();
|
||||
expect(item.id).toMatch(/^pending_/);
|
||||
});
|
||||
|
||||
it('usa id proporcionado', () => {
|
||||
const item = createPendingItem({ id: 'custom_id', query: 'provoleta' });
|
||||
expect(item.id).toBe('custom_id');
|
||||
});
|
||||
|
||||
it('usa status NEEDS_TYPE por defecto', () => {
|
||||
const item = createPendingItem({ query: 'provoleta' });
|
||||
expect(item.status).toBe(PendingStatus.NEEDS_TYPE);
|
||||
});
|
||||
|
||||
it('acepta status personalizado', () => {
|
||||
const item = createPendingItem({
|
||||
query: 'provoleta',
|
||||
status: PendingStatus.NEEDS_QUANTITY
|
||||
});
|
||||
expect(item.status).toBe(PendingStatus.NEEDS_QUANTITY);
|
||||
});
|
||||
|
||||
it('inicializa campos opcionales a null/[]', () => {
|
||||
const item = createPendingItem({ query: 'provoleta' });
|
||||
expect(item.candidates).toEqual([]);
|
||||
expect(item.selected_woo_id).toBeNull();
|
||||
expect(item.selected_name).toBeNull();
|
||||
expect(item.qty).toBeNull();
|
||||
});
|
||||
|
||||
it('guarda requested_qty y requested_unit', () => {
|
||||
const item = createPendingItem({
|
||||
query: 'vacío',
|
||||
requested_qty: 2,
|
||||
requested_unit: 'kg'
|
||||
});
|
||||
expect(item.requested_qty).toBe(2);
|
||||
expect(item.requested_unit).toBe('kg');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// moveReadyToCart
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('moveReadyToCart', () => {
|
||||
it('mueve items READY al cart', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [
|
||||
createPendingItem({
|
||||
id: 'p1',
|
||||
query: 'provoleta',
|
||||
status: PendingStatus.READY,
|
||||
selected_woo_id: 123,
|
||||
selected_name: 'Provoleta',
|
||||
qty: 2,
|
||||
unit: 'unit',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveReadyToCart(order);
|
||||
expect(result.cart).toHaveLength(1);
|
||||
expect(result.cart[0].woo_id).toBe(123);
|
||||
expect(result.cart[0].qty).toBe(2);
|
||||
expect(result.pending).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ignora items NEEDS_TYPE', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [
|
||||
createPendingItem({
|
||||
id: 'p1',
|
||||
query: 'provoleta',
|
||||
status: PendingStatus.NEEDS_TYPE,
|
||||
candidates: [{ woo_id: 1, name: 'A' }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveReadyToCart(order);
|
||||
expect(result.cart).toHaveLength(0);
|
||||
expect(result.pending).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('ignora items NEEDS_QUANTITY', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [
|
||||
createPendingItem({
|
||||
id: 'p1',
|
||||
query: 'vacío',
|
||||
status: PendingStatus.NEEDS_QUANTITY,
|
||||
selected_woo_id: 123,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveReadyToCart(order);
|
||||
expect(result.cart).toHaveLength(0);
|
||||
expect(result.pending).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('actualiza item existente en cart', () => {
|
||||
const order = {
|
||||
cart: [{ woo_id: 123, qty: 1, unit: 'unit', name: 'Provoleta' }],
|
||||
pending: [
|
||||
createPendingItem({
|
||||
id: 'p1',
|
||||
query: 'provoleta',
|
||||
status: PendingStatus.READY,
|
||||
selected_woo_id: 123,
|
||||
selected_name: 'Provoleta',
|
||||
qty: 3,
|
||||
unit: 'unit',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveReadyToCart(order);
|
||||
expect(result.cart).toHaveLength(1);
|
||||
expect(result.cart[0].qty).toBe(3); // Actualizado
|
||||
});
|
||||
|
||||
it('maneja order null', () => {
|
||||
const result = moveReadyToCart(null);
|
||||
expect(result).toEqual(createEmptyOrder());
|
||||
});
|
||||
|
||||
it('no mueve items sin selected_woo_id', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [
|
||||
createPendingItem({
|
||||
id: 'p1',
|
||||
query: 'algo',
|
||||
status: PendingStatus.READY,
|
||||
selected_woo_id: null,
|
||||
qty: 1,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveReadyToCart(order);
|
||||
expect(result.cart).toHaveLength(0);
|
||||
expect(result.pending).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('no mueve items sin qty', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [
|
||||
createPendingItem({
|
||||
id: 'p1',
|
||||
query: 'algo',
|
||||
status: PendingStatus.READY,
|
||||
selected_woo_id: 123,
|
||||
qty: null,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveReadyToCart(order);
|
||||
expect(result.cart).toHaveLength(0);
|
||||
expect(result.pending).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// getNextPendingItem
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('getNextPendingItem', () => {
|
||||
it('retorna item NEEDS_TYPE', () => {
|
||||
const order = {
|
||||
pending: [
|
||||
createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.NEEDS_TYPE }),
|
||||
],
|
||||
};
|
||||
const next = getNextPendingItem(order);
|
||||
expect(next).not.toBeNull();
|
||||
expect(next.id).toBe('p1');
|
||||
});
|
||||
|
||||
it('retorna item NEEDS_QUANTITY', () => {
|
||||
const order = {
|
||||
pending: [
|
||||
createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.NEEDS_QUANTITY }),
|
||||
],
|
||||
};
|
||||
const next = getNextPendingItem(order);
|
||||
expect(next).not.toBeNull();
|
||||
expect(next.id).toBe('p1');
|
||||
});
|
||||
|
||||
it('ignora items READY', () => {
|
||||
const order = {
|
||||
pending: [
|
||||
createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.READY }),
|
||||
],
|
||||
};
|
||||
const next = getNextPendingItem(order);
|
||||
expect(next).toBeNull();
|
||||
});
|
||||
|
||||
it('retorna el primero que necesita clarificación', () => {
|
||||
const order = {
|
||||
pending: [
|
||||
createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.READY }),
|
||||
createPendingItem({ id: 'p2', query: 'b', status: PendingStatus.NEEDS_TYPE }),
|
||||
createPendingItem({ id: 'p3', query: 'c', status: PendingStatus.NEEDS_QUANTITY }),
|
||||
],
|
||||
};
|
||||
const next = getNextPendingItem(order);
|
||||
expect(next.id).toBe('p2'); // Primero NEEDS_TYPE
|
||||
});
|
||||
|
||||
it('retorna null si no hay pending', () => {
|
||||
expect(getNextPendingItem({ pending: [] })).toBeNull();
|
||||
expect(getNextPendingItem({})).toBeNull();
|
||||
expect(getNextPendingItem(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// updatePendingItem
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('updatePendingItem', () => {
|
||||
it('actualiza campos por id', () => {
|
||||
const order = {
|
||||
pending: [
|
||||
createPendingItem({ id: 'p1', query: 'provoleta', status: PendingStatus.NEEDS_TYPE }),
|
||||
],
|
||||
};
|
||||
|
||||
const result = updatePendingItem(order, 'p1', {
|
||||
status: PendingStatus.READY,
|
||||
selected_woo_id: 123
|
||||
});
|
||||
|
||||
expect(result.pending[0].status).toBe(PendingStatus.READY);
|
||||
expect(result.pending[0].selected_woo_id).toBe(123);
|
||||
expect(result.pending[0].query).toBe('provoleta'); // No modificado
|
||||
});
|
||||
|
||||
it('no muta el original', () => {
|
||||
const original = {
|
||||
pending: [
|
||||
createPendingItem({ id: 'p1', query: 'provoleta', status: PendingStatus.NEEDS_TYPE }),
|
||||
],
|
||||
};
|
||||
|
||||
updatePendingItem(original, 'p1', { status: PendingStatus.READY });
|
||||
|
||||
expect(original.pending[0].status).toBe(PendingStatus.NEEDS_TYPE);
|
||||
});
|
||||
|
||||
it('no modifica si id no existe', () => {
|
||||
const order = {
|
||||
pending: [
|
||||
createPendingItem({ id: 'p1', query: 'provoleta' }),
|
||||
],
|
||||
};
|
||||
|
||||
const result = updatePendingItem(order, 'inexistente', { status: PendingStatus.READY });
|
||||
|
||||
expect(result.pending[0].status).toBe(PendingStatus.NEEDS_TYPE);
|
||||
});
|
||||
|
||||
it('maneja order sin pending', () => {
|
||||
const result = updatePendingItem({}, 'p1', { status: PendingStatus.READY });
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// addPendingItem
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('addPendingItem', () => {
|
||||
it('agrega item a pending existente', () => {
|
||||
const order = { cart: [], pending: [] };
|
||||
const newItem = createPendingItem({ query: 'provoleta' });
|
||||
|
||||
const result = addPendingItem(order, newItem);
|
||||
|
||||
expect(result.pending).toHaveLength(1);
|
||||
expect(result.pending[0].query).toBe('provoleta');
|
||||
});
|
||||
|
||||
it('preserva items existentes', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [createPendingItem({ query: 'vacío' })]
|
||||
};
|
||||
const newItem = createPendingItem({ query: 'provoleta' });
|
||||
|
||||
const result = addPendingItem(order, newItem);
|
||||
|
||||
expect(result.pending).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('crea order si es null', () => {
|
||||
const newItem = createPendingItem({ query: 'provoleta' });
|
||||
|
||||
const result = addPendingItem(null, newItem);
|
||||
|
||||
expect(result.cart).toEqual([]);
|
||||
expect(result.pending).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// removeCartItem
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('removeCartItem', () => {
|
||||
const orderWithItems = {
|
||||
cart: [
|
||||
{ woo_id: 1, name: 'Provoleta clásica', qty: 2, unit: 'unit' },
|
||||
{ woo_id: 2, name: 'Vacío premium', qty: 1.5, unit: 'kg' },
|
||||
{ woo_id: 3, name: 'Bife de chorizo', qty: 2, unit: 'kg' },
|
||||
],
|
||||
pending: [],
|
||||
};
|
||||
|
||||
it('remueve item por nombre exacto', () => {
|
||||
const { order, removed } = removeCartItem(orderWithItems, 'vacío premium');
|
||||
|
||||
expect(removed).not.toBeNull();
|
||||
expect(removed.name).toBe('Vacío premium');
|
||||
expect(order.cart).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('remueve item por palabra clave', () => {
|
||||
const { order, removed } = removeCartItem(orderWithItems, 'provoleta');
|
||||
|
||||
expect(removed).not.toBeNull();
|
||||
expect(removed.name).toBe('Provoleta clásica');
|
||||
});
|
||||
|
||||
it('remueve item por múltiples palabras', () => {
|
||||
const { order, removed } = removeCartItem(orderWithItems, 'bife chorizo');
|
||||
|
||||
expect(removed).not.toBeNull();
|
||||
expect(removed.name).toBe('Bife de chorizo');
|
||||
});
|
||||
|
||||
it('retorna null si no encuentra match', () => {
|
||||
const { order, removed } = removeCartItem(orderWithItems, 'chimichurri');
|
||||
|
||||
expect(removed).toBeNull();
|
||||
expect(order.cart).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('maneja cart vacío', () => {
|
||||
const { order, removed } = removeCartItem({ cart: [] }, 'provoleta');
|
||||
|
||||
expect(removed).toBeNull();
|
||||
});
|
||||
|
||||
it('maneja order null', () => {
|
||||
const { order, removed } = removeCartItem(null, 'provoleta');
|
||||
|
||||
expect(removed).toBeNull();
|
||||
});
|
||||
|
||||
it('maneja productQuery vacío', () => {
|
||||
const { order, removed } = removeCartItem(orderWithItems, '');
|
||||
|
||||
expect(removed).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// updateCartItemQuantity
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('updateCartItemQuantity', () => {
|
||||
const orderWithItems = {
|
||||
cart: [
|
||||
{ woo_id: 1, name: 'Vacío premium', qty: 1.5, unit: 'kg' },
|
||||
{ woo_id: 2, name: 'Provoleta', qty: 2, unit: 'unit' },
|
||||
],
|
||||
pending: [],
|
||||
};
|
||||
|
||||
it('actualiza cantidad', () => {
|
||||
const { order, updated } = updateCartItemQuantity(orderWithItems, 'vacío', 3);
|
||||
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated.qty).toBe(3);
|
||||
expect(order.cart.find(i => i.name === 'Vacío premium').qty).toBe(3);
|
||||
});
|
||||
|
||||
it('actualiza cantidad y unidad', () => {
|
||||
const { order, updated } = updateCartItemQuantity(orderWithItems, 'vacío', 500, 'g');
|
||||
|
||||
expect(updated.qty).toBe(500);
|
||||
expect(updated.unit).toBe('g');
|
||||
});
|
||||
|
||||
it('retorna null si no encuentra', () => {
|
||||
const { order, updated } = updateCartItemQuantity(orderWithItems, 'chorizo', 2);
|
||||
|
||||
expect(updated).toBeNull();
|
||||
});
|
||||
|
||||
it('maneja cart vacío', () => {
|
||||
const { order, updated } = updateCartItemQuantity({ cart: [] }, 'vacío', 2);
|
||||
|
||||
expect(updated).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// migrateOldContext
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('migrateOldContext', () => {
|
||||
it('retorna orden vacía para ctx null', () => {
|
||||
const result = migrateOldContext(null);
|
||||
expect(result).toEqual(createEmptyOrder());
|
||||
});
|
||||
|
||||
it('retorna order existente si ya está migrado', () => {
|
||||
const ctx = {
|
||||
order: {
|
||||
cart: [{ woo_id: 1, qty: 1 }],
|
||||
pending: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = migrateOldContext(ctx);
|
||||
expect(result.cart).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('migra order_basket', () => {
|
||||
const ctx = {
|
||||
order_basket: {
|
||||
items: [
|
||||
{ product_id: 123, quantity: 2, unit: 'kg', label: 'Vacío', price: 5000 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = migrateOldContext(ctx);
|
||||
expect(result.cart).toHaveLength(1);
|
||||
expect(result.cart[0].woo_id).toBe(123);
|
||||
expect(result.cart[0].qty).toBe(2);
|
||||
expect(result.cart[0].name).toBe('Vacío');
|
||||
});
|
||||
|
||||
it('migra pending_items', () => {
|
||||
const ctx = {
|
||||
pending_items: [
|
||||
{
|
||||
id: 'p1',
|
||||
query: 'provoleta',
|
||||
status: 'needs_type',
|
||||
candidates: [{ woo_product_id: 1, name: 'Provoleta A' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = migrateOldContext(ctx);
|
||||
expect(result.pending).toHaveLength(1);
|
||||
expect(result.pending[0].status).toBe(PendingStatus.NEEDS_TYPE);
|
||||
});
|
||||
|
||||
it('migra checkout info', () => {
|
||||
const ctx = {
|
||||
payment_method: 'cash',
|
||||
shipping_method: 'delivery',
|
||||
delivery_address: { text: 'Calle Falsa 123' },
|
||||
woo_order_id: 456,
|
||||
};
|
||||
|
||||
const result = migrateOldContext(ctx);
|
||||
expect(result.payment_type).toBe('cash');
|
||||
expect(result.is_delivery).toBe(true);
|
||||
expect(result.shipping_address).toBe('Calle Falsa 123');
|
||||
expect(result.woo_order_id).toBe(456);
|
||||
});
|
||||
|
||||
it('migra shipping_method pickup', () => {
|
||||
const ctx = {
|
||||
shipping_method: 'pickup',
|
||||
};
|
||||
|
||||
const result = migrateOldContext(ctx);
|
||||
expect(result.is_delivery).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// formatCartForDisplay
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('formatCartForDisplay', () => {
|
||||
it('formatea items con cantidad kg', () => {
|
||||
const order = {
|
||||
cart: [
|
||||
{ woo_id: 1, name: 'Vacío', qty: 1.5, unit: 'kg' },
|
||||
],
|
||||
pending: [],
|
||||
};
|
||||
|
||||
const display = formatCartForDisplay(order);
|
||||
expect(display).toContain('1.5kg de Vacío');
|
||||
});
|
||||
|
||||
it('formatea items con cantidad g', () => {
|
||||
const order = {
|
||||
cart: [
|
||||
{ woo_id: 1, name: 'Jamón', qty: 500, unit: 'g' },
|
||||
],
|
||||
pending: [],
|
||||
};
|
||||
|
||||
const display = formatCartForDisplay(order);
|
||||
expect(display).toContain('500g de Jamón');
|
||||
});
|
||||
|
||||
it('formatea items por unidad', () => {
|
||||
const order = {
|
||||
cart: [
|
||||
{ woo_id: 1, name: 'Provoleta', qty: 3, unit: 'unit' },
|
||||
],
|
||||
pending: [],
|
||||
};
|
||||
|
||||
const display = formatCartForDisplay(order);
|
||||
expect(display).toContain('3 de Provoleta');
|
||||
});
|
||||
|
||||
it('muestra pending items NEEDS_TYPE', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [
|
||||
createPendingItem({ query: 'provoleta', status: PendingStatus.NEEDS_TYPE }),
|
||||
],
|
||||
};
|
||||
|
||||
const display = formatCartForDisplay(order);
|
||||
expect(display).toContain('provoleta');
|
||||
expect(display).toContain('pendiente');
|
||||
});
|
||||
|
||||
it('muestra pending items NEEDS_QUANTITY', () => {
|
||||
const order = {
|
||||
cart: [],
|
||||
pending: [
|
||||
createPendingItem({
|
||||
query: 'vacío',
|
||||
status: PendingStatus.NEEDS_QUANTITY,
|
||||
selected_name: 'Vacío premium',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const display = formatCartForDisplay(order);
|
||||
expect(display).toContain('Vacío premium');
|
||||
expect(display).toContain('falta cantidad');
|
||||
});
|
||||
|
||||
it('muestra mensaje de carrito vacío', () => {
|
||||
const order = { cart: [], pending: [] };
|
||||
|
||||
const display = formatCartForDisplay(order);
|
||||
expect(display).toBe('Tu carrito está vacío.');
|
||||
});
|
||||
|
||||
it('maneja order null', () => {
|
||||
const display = formatCartForDisplay(null);
|
||||
expect(display).toBe('Tu carrito está vacío.');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// formatOptionsForDisplay
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('formatOptionsForDisplay', () => {
|
||||
it('lista opciones numeradas', () => {
|
||||
const pendingItem = createPendingItem({
|
||||
query: 'provoleta',
|
||||
candidates: [
|
||||
{ woo_id: 1, name: 'Provoleta clásica' },
|
||||
{ woo_id: 2, name: 'Provoleta de búfala' },
|
||||
],
|
||||
});
|
||||
|
||||
const { question, options } = formatOptionsForDisplay(pendingItem);
|
||||
|
||||
expect(question).toContain('1) Provoleta clásica');
|
||||
expect(question).toContain('2) Provoleta de búfala');
|
||||
expect(options).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('muestra "Mostrame más" si hay más de pageSize', () => {
|
||||
const candidates = Array.from({ length: 15 }, (_, i) => ({
|
||||
woo_id: i,
|
||||
name: `Producto ${i}`,
|
||||
}));
|
||||
const pendingItem = createPendingItem({
|
||||
query: 'producto',
|
||||
candidates,
|
||||
});
|
||||
|
||||
const { question } = formatOptionsForDisplay(pendingItem, 12);
|
||||
|
||||
expect(question).toContain('13) Mostrame más');
|
||||
});
|
||||
|
||||
it('no muestra "Mostrame más" si hay menos de pageSize', () => {
|
||||
const pendingItem = createPendingItem({
|
||||
query: 'provoleta',
|
||||
candidates: [
|
||||
{ woo_id: 1, name: 'A' },
|
||||
{ woo_id: 2, name: 'B' },
|
||||
],
|
||||
});
|
||||
|
||||
const { question } = formatOptionsForDisplay(pendingItem);
|
||||
|
||||
expect(question).not.toContain('Mostrame más');
|
||||
});
|
||||
|
||||
it('muestra mensaje de no encontrado sin candidatos', () => {
|
||||
const pendingItem = createPendingItem({
|
||||
query: 'xyz',
|
||||
candidates: [],
|
||||
});
|
||||
|
||||
const { question, options } = formatOptionsForDisplay(pendingItem);
|
||||
|
||||
expect(question).toContain('No encontré');
|
||||
expect(question).toContain('xyz');
|
||||
expect(options).toEqual([]);
|
||||
});
|
||||
|
||||
it('maneja pendingItem null', () => {
|
||||
const { question } = formatOptionsForDisplay(null);
|
||||
expect(question).toContain('No encontré');
|
||||
});
|
||||
});
|
||||
448
src/modules/3-turn-engine/stateHandlers/utils.test.js
Normal file
448
src/modules/3-turn-engine/stateHandlers/utils.test.js
Normal file
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* Tests para utils.js
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
inferDefaultUnit,
|
||||
parseIndexSelection,
|
||||
isShowMoreRequest,
|
||||
isShowOptionsRequest,
|
||||
findMatchingCandidate,
|
||||
isEscapeRequest,
|
||||
normalizeUnit,
|
||||
unitAskFor,
|
||||
} from './utils.js';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// parseIndexSelection
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('parseIndexSelection', () => {
|
||||
describe('números directos', () => {
|
||||
it('parsea número simple', () => {
|
||||
expect(parseIndexSelection('2')).toBe(2);
|
||||
expect(parseIndexSelection('5')).toBe(5);
|
||||
expect(parseIndexSelection('10')).toBe(10);
|
||||
});
|
||||
|
||||
it('parsea número en frase', () => {
|
||||
expect(parseIndexSelection('quiero el 2')).toBe(2);
|
||||
expect(parseIndexSelection('dame la opción 3')).toBe(3);
|
||||
expect(parseIndexSelection('el número 7 por favor')).toBe(7);
|
||||
});
|
||||
|
||||
it('parsea números de dos dígitos', () => {
|
||||
expect(parseIndexSelection('el 12')).toBe(12);
|
||||
expect(parseIndexSelection('opción 15')).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ordinales en español', () => {
|
||||
it('parsea ordinales masculinos', () => {
|
||||
expect(parseIndexSelection('el primero')).toBe(1);
|
||||
expect(parseIndexSelection('segundo')).toBe(2);
|
||||
expect(parseIndexSelection('tercero')).toBe(3);
|
||||
expect(parseIndexSelection('cuarto')).toBe(4);
|
||||
expect(parseIndexSelection('quinto')).toBe(5);
|
||||
expect(parseIndexSelection('sexto')).toBe(6);
|
||||
expect(parseIndexSelection('séptimo')).toBe(7);
|
||||
expect(parseIndexSelection('octavo')).toBe(8);
|
||||
expect(parseIndexSelection('noveno')).toBe(9);
|
||||
expect(parseIndexSelection('décimo')).toBe(10);
|
||||
});
|
||||
|
||||
it('parsea ordinales femeninos', () => {
|
||||
expect(parseIndexSelection('la primera')).toBe(1);
|
||||
expect(parseIndexSelection('segunda')).toBe(2);
|
||||
expect(parseIndexSelection('tercera')).toBe(3);
|
||||
expect(parseIndexSelection('cuarta')).toBe(4);
|
||||
expect(parseIndexSelection('quinta')).toBe(5);
|
||||
expect(parseIndexSelection('sexta')).toBe(6);
|
||||
expect(parseIndexSelection('séptima')).toBe(7);
|
||||
expect(parseIndexSelection('octava')).toBe(8);
|
||||
expect(parseIndexSelection('novena')).toBe(9);
|
||||
expect(parseIndexSelection('décima')).toBe(10);
|
||||
});
|
||||
|
||||
it('parsea ordinales sin tilde', () => {
|
||||
expect(parseIndexSelection('septimo')).toBe(7);
|
||||
expect(parseIndexSelection('decimo')).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('casos sin selección', () => {
|
||||
it('retorna null para texto sin número ni ordinal', () => {
|
||||
expect(parseIndexSelection('hola')).toBeNull();
|
||||
expect(parseIndexSelection('quiero provoleta')).toBeNull();
|
||||
expect(parseIndexSelection('no sé')).toBeNull();
|
||||
});
|
||||
|
||||
it('retorna null para valores vacíos', () => {
|
||||
expect(parseIndexSelection('')).toBeNull();
|
||||
expect(parseIndexSelection(null)).toBeNull();
|
||||
expect(parseIndexSelection(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// normalizeUnit
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('normalizeUnit', () => {
|
||||
describe('kilogramos', () => {
|
||||
it('normaliza kg', () => {
|
||||
expect(normalizeUnit('kg')).toBe('kg');
|
||||
expect(normalizeUnit('KG')).toBe('kg');
|
||||
});
|
||||
|
||||
it('normaliza kilo/kilos', () => {
|
||||
expect(normalizeUnit('kilo')).toBe('kg');
|
||||
expect(normalizeUnit('kilos')).toBe('kg');
|
||||
expect(normalizeUnit('KILOS')).toBe('kg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gramos', () => {
|
||||
it('normaliza g', () => {
|
||||
expect(normalizeUnit('g')).toBe('g');
|
||||
expect(normalizeUnit('G')).toBe('g');
|
||||
});
|
||||
|
||||
it('normaliza gramo/gramos', () => {
|
||||
expect(normalizeUnit('gramo')).toBe('g');
|
||||
expect(normalizeUnit('gramos')).toBe('g');
|
||||
expect(normalizeUnit('GRAMOS')).toBe('g');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unidades', () => {
|
||||
it('normaliza unit', () => {
|
||||
expect(normalizeUnit('unit')).toBe('unit');
|
||||
});
|
||||
|
||||
it('normaliza unidad/unidades', () => {
|
||||
expect(normalizeUnit('unidad')).toBe('unit');
|
||||
expect(normalizeUnit('unidades')).toBe('unit');
|
||||
expect(normalizeUnit('UNIDADES')).toBe('unit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('valores inválidos', () => {
|
||||
it('retorna null para unidades desconocidas', () => {
|
||||
expect(normalizeUnit('litro')).toBeNull();
|
||||
expect(normalizeUnit('docena')).toBeNull();
|
||||
expect(normalizeUnit('xyz')).toBeNull();
|
||||
});
|
||||
|
||||
it('retorna null para valores vacíos', () => {
|
||||
expect(normalizeUnit('')).toBeNull();
|
||||
expect(normalizeUnit(null)).toBeNull();
|
||||
expect(normalizeUnit(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// inferDefaultUnit
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('inferDefaultUnit', () => {
|
||||
describe('productos que se venden por unidad', () => {
|
||||
it('detecta provoleta/queso por nombre', () => {
|
||||
expect(inferDefaultUnit({ name: 'Provoleta clásica', categories: [] })).toBe('unit');
|
||||
expect(inferDefaultUnit({ name: 'Queso provolone', categories: [] })).toBe('unit');
|
||||
expect(inferDefaultUnit({ name: 'Pan de campo', categories: [] })).toBe('unit');
|
||||
});
|
||||
|
||||
it('detecta bebidas por nombre', () => {
|
||||
expect(inferDefaultUnit({ name: 'Vino Malbec', categories: [] })).toBe('unit');
|
||||
expect(inferDefaultUnit({ name: 'Cerveza artesanal', categories: [] })).toBe('unit');
|
||||
expect(inferDefaultUnit({ name: 'Fernet Branca', categories: [] })).toBe('unit');
|
||||
});
|
||||
|
||||
it('detecta condimentos', () => {
|
||||
expect(inferDefaultUnit({ name: 'Chimichurri casero', categories: [] })).toBe('unit');
|
||||
expect(inferDefaultUnit({ name: 'Salsa criolla', categories: [] })).toBe('unit');
|
||||
});
|
||||
|
||||
it('detecta por categoría', () => {
|
||||
expect(inferDefaultUnit({
|
||||
name: 'Producto X',
|
||||
categories: [{ name: 'Vinos', slug: 'vinos' }]
|
||||
})).toBe('unit');
|
||||
expect(inferDefaultUnit({
|
||||
name: 'Producto Y',
|
||||
categories: [{ name: 'Proveeduría', slug: 'proveeduria' }]
|
||||
})).toBe('unit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('productos que se venden por kg', () => {
|
||||
it('retorna kg para carnes', () => {
|
||||
expect(inferDefaultUnit({ name: 'Bife de chorizo', categories: [] })).toBe('kg');
|
||||
expect(inferDefaultUnit({ name: 'Vacío', categories: [] })).toBe('kg');
|
||||
expect(inferDefaultUnit({ name: 'Asado de tira', categories: [] })).toBe('kg');
|
||||
});
|
||||
|
||||
it('retorna kg por defecto', () => {
|
||||
expect(inferDefaultUnit({ name: 'Producto genérico', categories: [] })).toBe('kg');
|
||||
expect(inferDefaultUnit({ name: '', categories: [] })).toBe('kg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('maneja categories no array', () => {
|
||||
expect(inferDefaultUnit({ name: 'Vino', categories: null })).toBe('unit');
|
||||
expect(inferDefaultUnit({ name: 'Vino', categories: undefined })).toBe('unit');
|
||||
});
|
||||
|
||||
it('maneja name vacío o null', () => {
|
||||
expect(inferDefaultUnit({ name: null, categories: [] })).toBe('kg');
|
||||
expect(inferDefaultUnit({ name: undefined, categories: [] })).toBe('kg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// isShowMoreRequest
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('isShowMoreRequest', () => {
|
||||
describe('detecta pedidos de más opciones', () => {
|
||||
it('detecta "mostrame más"', () => {
|
||||
expect(isShowMoreRequest('mostrame más')).toBe(true);
|
||||
expect(isShowMoreRequest('mostrame mas')).toBe(true);
|
||||
expect(isShowMoreRequest('mostra más')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "más opciones"', () => {
|
||||
expect(isShowMoreRequest('más opciones')).toBe(true);
|
||||
expect(isShowMoreRequest('mas opciones')).toBe(true);
|
||||
expect(isShowMoreRequest('quiero más opciones')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "siguientes"', () => {
|
||||
expect(isShowMoreRequest('siguientes')).toBe(true);
|
||||
expect(isShowMoreRequest('siguiente')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "otras opciones"', () => {
|
||||
expect(isShowMoreRequest('otras opciones')).toBe(true);
|
||||
expect(isShowMoreRequest('hay otras?')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "ver más"', () => {
|
||||
expect(isShowMoreRequest('ver más')).toBe(true);
|
||||
expect(isShowMoreRequest('ver mas')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "qué más hay"', () => {
|
||||
expect(isShowMoreRequest('qué más hay')).toBe(true);
|
||||
expect(isShowMoreRequest('que mas hay')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no detecta falsos positivos', () => {
|
||||
it('no detecta frases normales', () => {
|
||||
expect(isShowMoreRequest('quiero el primero')).toBe(false);
|
||||
expect(isShowMoreRequest('dame provoleta')).toBe(false);
|
||||
expect(isShowMoreRequest('hola')).toBe(false);
|
||||
});
|
||||
|
||||
it('no detecta valores vacíos', () => {
|
||||
expect(isShowMoreRequest('')).toBe(false);
|
||||
expect(isShowMoreRequest(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// isShowOptionsRequest
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('isShowOptionsRequest', () => {
|
||||
describe('detecta pedidos de ver opciones', () => {
|
||||
it('detecta "qué opciones"', () => {
|
||||
expect(isShowOptionsRequest('qué opciones tenés?')).toBe(true);
|
||||
expect(isShowOptionsRequest('que opciones hay')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "cuáles tenés"', () => {
|
||||
expect(isShowOptionsRequest('cuáles tenés')).toBe(true);
|
||||
expect(isShowOptionsRequest('cuales son')).toBe(true);
|
||||
expect(isShowOptionsRequest('cuáles hay')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "qué hay"', () => {
|
||||
expect(isShowOptionsRequest('qué hay')).toBe(true);
|
||||
expect(isShowOptionsRequest('que hay')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "qué tenés"', () => {
|
||||
expect(isShowOptionsRequest('qué tenés')).toBe(true);
|
||||
expect(isShowOptionsRequest('que tenes')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "ver opciones"', () => {
|
||||
expect(isShowOptionsRequest('ver opciones')).toBe(true);
|
||||
expect(isShowOptionsRequest('ver las opciones')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no detecta falsos positivos', () => {
|
||||
it('no detecta frases normales', () => {
|
||||
expect(isShowOptionsRequest('quiero el 2')).toBe(false);
|
||||
expect(isShowOptionsRequest('dame provoleta')).toBe(false);
|
||||
});
|
||||
|
||||
it('no detecta valores vacíos', () => {
|
||||
expect(isShowOptionsRequest('')).toBe(false);
|
||||
expect(isShowOptionsRequest(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// isEscapeRequest
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('isEscapeRequest', () => {
|
||||
describe('detecta pedidos de escape', () => {
|
||||
it('detecta "qué tengo"', () => {
|
||||
expect(isEscapeRequest('qué tengo')).toBe(true);
|
||||
expect(isEscapeRequest('que tengo en el carrito')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "mi carrito"', () => {
|
||||
expect(isEscapeRequest('mi carrito')).toBe(true);
|
||||
expect(isEscapeRequest('mi pedido')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "ver carrito"', () => {
|
||||
expect(isEscapeRequest('ver carrito')).toBe(true);
|
||||
expect(isEscapeRequest('ver pedido')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "listo"', () => {
|
||||
expect(isEscapeRequest('listo')).toBe(true);
|
||||
expect(isEscapeRequest('ya está listo')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "confirmar"', () => {
|
||||
expect(isEscapeRequest('confirmar')).toBe(true);
|
||||
expect(isEscapeRequest('quiero confirmar')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "cancelar"', () => {
|
||||
expect(isEscapeRequest('cancelar')).toBe(true);
|
||||
expect(isEscapeRequest('quiero cancelar')).toBe(true);
|
||||
});
|
||||
|
||||
it('detecta "eso es todo"', () => {
|
||||
expect(isEscapeRequest('eso es todo')).toBe(true);
|
||||
expect(isEscapeRequest('eso todo')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no detecta falsos positivos', () => {
|
||||
it('no detecta productos', () => {
|
||||
expect(isEscapeRequest('quiero provoleta')).toBe(false);
|
||||
expect(isEscapeRequest('dame 2kg de vacío')).toBe(false);
|
||||
});
|
||||
|
||||
it('no detecta valores vacíos', () => {
|
||||
expect(isEscapeRequest('')).toBe(false);
|
||||
expect(isEscapeRequest(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// findMatchingCandidate
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('findMatchingCandidate', () => {
|
||||
const candidates = [
|
||||
{ name: 'Provoleta de bufala' },
|
||||
{ name: 'Provoleta clasica' },
|
||||
{ name: 'Queso provolone' },
|
||||
{ name: 'Chimichurri casero' },
|
||||
];
|
||||
|
||||
describe('encuentra matches', () => {
|
||||
it('encuentra match exacto de palabra', () => {
|
||||
const match = findMatchingCandidate(candidates, 'bufala');
|
||||
expect(match).not.toBeNull();
|
||||
expect(match.index).toBe(0);
|
||||
expect(match.candidate.name).toBe('Provoleta de bufala');
|
||||
});
|
||||
|
||||
it('encuentra match parcial', () => {
|
||||
const match = findMatchingCandidate(candidates, 'clasica');
|
||||
expect(match).not.toBeNull();
|
||||
expect(match.index).toBe(1);
|
||||
});
|
||||
|
||||
it('da bonus por match completo', () => {
|
||||
const match = findMatchingCandidate(candidates, 'provoleta clasica');
|
||||
expect(match).not.toBeNull();
|
||||
expect(match.index).toBe(1);
|
||||
expect(match.score).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('encuentra mejor match entre varios', () => {
|
||||
const match = findMatchingCandidate(candidates, 'provoleta');
|
||||
expect(match).not.toBeNull();
|
||||
// Ambos tienen "provoleta", debe elegir uno
|
||||
expect([0, 1]).toContain(match.index);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no encuentra match', () => {
|
||||
it('retorna null sin coincidencia', () => {
|
||||
expect(findMatchingCandidate(candidates, 'chorizo')).toBeNull();
|
||||
expect(findMatchingCandidate(candidates, 'vino')).toBeNull();
|
||||
});
|
||||
|
||||
it('retorna null para candidates vacío', () => {
|
||||
expect(findMatchingCandidate([], 'provoleta')).toBeNull();
|
||||
expect(findMatchingCandidate(null, 'provoleta')).toBeNull();
|
||||
});
|
||||
|
||||
it('retorna null para texto vacío', () => {
|
||||
expect(findMatchingCandidate(candidates, '')).toBeNull();
|
||||
expect(findMatchingCandidate(candidates, null)).toBeNull();
|
||||
});
|
||||
|
||||
it('texto corto puede matchear por inclusión completa', () => {
|
||||
// "de" tiene 2 caracteres, se filtra como palabra pero si el texto completo
|
||||
// está en el nombre, da bonus y matchea
|
||||
const match = findMatchingCandidate(candidates, 'de');
|
||||
// La función da bonus si el texto completo está en el nombre
|
||||
expect(match).not.toBeNull();
|
||||
expect(match.score).toBe(2); // bonus por match completo
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// unitAskFor
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('unitAskFor', () => {
|
||||
it('genera pregunta para unidades', () => {
|
||||
expect(unitAskFor('unit')).toBe('¿Cuántas unidades querés?');
|
||||
});
|
||||
|
||||
it('genera pregunta para gramos', () => {
|
||||
expect(unitAskFor('g')).toBe('¿Cuántos gramos querés?');
|
||||
});
|
||||
|
||||
it('genera pregunta para kilos (default)', () => {
|
||||
expect(unitAskFor('kg')).toBe('¿Cuántos kilos querés?');
|
||||
expect(unitAskFor(null)).toBe('¿Cuántos kilos querés?');
|
||||
expect(unitAskFor(undefined)).toBe('¿Cuántos kilos querés?');
|
||||
expect(unitAskFor('otro')).toBe('¿Cuántos kilos querés?');
|
||||
});
|
||||
});
|
||||
13
vitest.config.js
Normal file
13
vitest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.test.js'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user