A66 WIP: AgendaEventDialog quebrado em 5 composables + 265 specs + V2 esqueleto
Sub-sessao 1 entregue (composables): - agendaEventHelpers (262L) — utilitarios puros (date, format, parse) - useAgendaEventComposer (485L) — montagem do form + validacao - useAgendaEventActions (387L) — save/delete/cancel/move actions - useAgendaEventPickerBilling (378L) — pickers (terapeuta, servico, convenio) + calculo de billing - useAgendaEventLifecycle (474L) — open/close/dirty state + autosave - 5 specs em __tests__/ (75+76+28+43+43 = 265 testes), 495/495 passing AgendaEventDialog: 3522 -> 2632 linhas (-25%) consumindo os composables. Backup byte-identico em AgendaEventDialog.vue.bak pra rollback. Sub-sessao 2 entregue (esqueleto, NAO TESTADO): - AgendaEventDialogV2 (~1100L, 3 zonas: PACIENTE/QUANDO/O QUE) - Preview em /preview/agenda-dialog-v2 com 5 cenarios - Rota em routes.misc.js - User testou e nao gostou do design — aguarda feedback especifico pra iteracao na sub-sessao 3 (migracao nos 9 consumers). Dialogs auxiliares novos pro AgendaEventDialog: - InsurancePlanQuickCreateDialog (criar convenio inline) - ServiceQuickCreateDialog (criar tipo de sessao inline) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,701 @@
|
||||
/**
|
||||
* useAgendaEventComposer.spec.js — A66 sub-sessão 1B
|
||||
*
|
||||
* Cobre o composable factory extraído do AgendaEventDialog.
|
||||
* Foco do contrato: refs reativos + computeds derivados são consistentes
|
||||
* com o comportamento original do .vue (matriz de inputs, edge cases).
|
||||
*
|
||||
* Não cobre: watchers e handlers (1C — fica no .vue ainda).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useAgendaEventComposer } from '../useAgendaEventComposer';
|
||||
|
||||
// Mock de getPatientAgendaPermissions — o composable importa direto
|
||||
vi.mock('@/composables/usePatientLifecycle', () => ({
|
||||
getPatientAgendaPermissions: (status) => {
|
||||
const norm = String(status || '').toLowerCase();
|
||||
if (norm === 'inativo' || norm === 'arquivado') {
|
||||
return { canCreateSession: false, canCreateRecurrence: false };
|
||||
}
|
||||
return { canCreateSession: true, canCreateRecurrence: true };
|
||||
}
|
||||
}));
|
||||
|
||||
// Helper: monta props default + emit fake
|
||||
function setup(overrides = {}, extras = {}) {
|
||||
const props = reactive({
|
||||
modelValue: false,
|
||||
eventRow: null,
|
||||
initialStartISO: '',
|
||||
initialEndISO: '',
|
||||
ownerId: 'owner-1',
|
||||
planOwnerId: '',
|
||||
allowOwnerEdit: false,
|
||||
ownerOptions: [],
|
||||
tenantId: 'tenant-1',
|
||||
commitmentOptions: [],
|
||||
presetCommitmentId: null,
|
||||
lockCommitment: false,
|
||||
restrictPatientsToOwner: false,
|
||||
patientScopeOwnerId: null,
|
||||
workRules: [],
|
||||
blockedDates: [],
|
||||
agendaSettings: { session_duration_min: 50, session_break_min: 0 },
|
||||
allEvents: [],
|
||||
pausasSemanais: [],
|
||||
feriados: [],
|
||||
newPatientRoute: '',
|
||||
...overrides
|
||||
});
|
||||
const emitted = [];
|
||||
const emit = (...args) => emitted.push(args);
|
||||
const composer = useAgendaEventComposer(props, emit, extras);
|
||||
return { props, emit, emitted, composer };
|
||||
}
|
||||
|
||||
const SESSION_COMMITMENT = { id: 'c-session', native_key: 'session', name: 'Sessão' };
|
||||
const MEETING_COMMITMENT = { id: 'c-meeting', native_key: 'meeting', name: 'Reunião' };
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
describe('visible (v-model)', () => {
|
||||
it('lê de props.modelValue', () => {
|
||||
const { composer, props } = setup({ modelValue: true });
|
||||
expect(composer.visible.value).toBe(true);
|
||||
props.modelValue = false;
|
||||
expect(composer.visible.value).toBe(false);
|
||||
});
|
||||
it('escrever emite update:modelValue', () => {
|
||||
const { composer, emitted } = setup();
|
||||
composer.visible.value = true;
|
||||
expect(emitted).toContainEqual(['update:modelValue', true]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEdit', () => {
|
||||
it('false sem eventRow', () => {
|
||||
const { composer } = setup({ eventRow: null });
|
||||
expect(composer.isEdit.value).toBe(false);
|
||||
});
|
||||
it('true com id', () => {
|
||||
const { composer } = setup({ eventRow: { id: 'evt-1' } });
|
||||
expect(composer.isEdit.value).toBe(true);
|
||||
});
|
||||
it('true com is_occurrence (sem id)', () => {
|
||||
const { composer } = setup({ eventRow: { is_occurrence: true } });
|
||||
expect(composer.isEdit.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowBack', () => {
|
||||
it('true por default', () => {
|
||||
const { composer } = setup();
|
||||
expect(composer.allowBack.value).toBe(true);
|
||||
});
|
||||
it('false quando lockCommitment', () => {
|
||||
const { composer } = setup({ lockCommitment: true });
|
||||
expect(composer.allowBack.value).toBe(false);
|
||||
});
|
||||
it('false quando presetCommitmentId', () => {
|
||||
const { composer } = setup({ presetCommitmentId: 'c-1' });
|
||||
expect(composer.allowBack.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSerie', () => {
|
||||
it('false sem indicadores de série', () => {
|
||||
const { composer } = setup({ eventRow: { id: 'evt-1' } });
|
||||
expect(composer.hasSerie.value).toBe(false);
|
||||
});
|
||||
it('true com recurrence_id', () => {
|
||||
const { composer } = setup({ eventRow: { id: 'evt-1', recurrence_id: 'r-1' } });
|
||||
expect(composer.hasSerie.value).toBe(true);
|
||||
});
|
||||
it('true com serie_id (legado)', () => {
|
||||
const { composer } = setup({ eventRow: { id: 'evt-1', serie_id: 's-1' } });
|
||||
expect(composer.hasSerie.value).toBe(true);
|
||||
});
|
||||
it('true com is_occurrence', () => {
|
||||
const { composer } = setup({ eventRow: { is_occurrence: true } });
|
||||
expect(composer.hasSerie.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFirstOccurrence', () => {
|
||||
it('false quando não é série', () => {
|
||||
const { composer } = setup();
|
||||
expect(composer.isFirstOccurrence.value).toBe(false);
|
||||
});
|
||||
it('true quando recurrence_date é a menor data da série', () => {
|
||||
const serieEvents = ref([
|
||||
{ recurrence_date: '2026-05-15' },
|
||||
{ recurrence_date: '2026-05-22' },
|
||||
{ recurrence_date: '2026-05-29' }
|
||||
]);
|
||||
const { composer } = setup(
|
||||
{ eventRow: { id: 'evt-1', recurrence_id: 'r-1', recurrence_date: '2026-05-15' } },
|
||||
{ serieEvents }
|
||||
);
|
||||
expect(composer.isFirstOccurrence.value).toBe(true);
|
||||
});
|
||||
it('false quando recurrence_date NÃO é a menor', () => {
|
||||
const serieEvents = ref([
|
||||
{ recurrence_date: '2026-05-15' },
|
||||
{ recurrence_date: '2026-05-22' }
|
||||
]);
|
||||
const { composer } = setup(
|
||||
{ eventRow: { id: 'evt-1', recurrence_id: 'r-1', recurrence_date: '2026-05-22' } },
|
||||
{ serieEvents }
|
||||
);
|
||||
expect(composer.isFirstOccurrence.value).toBe(false);
|
||||
});
|
||||
it('false quando serieEvents vazio', () => {
|
||||
const { composer } = setup(
|
||||
{ eventRow: { id: 'evt-1', recurrence_id: 'r-1', recurrence_date: '2026-05-15' } },
|
||||
{ serieEvents: ref([]) }
|
||||
);
|
||||
expect(composer.isFirstOccurrence.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editScopeOptions', () => {
|
||||
it('retorna 4 opções', () => {
|
||||
const { composer } = setup();
|
||||
expect(composer.editScopeOptions.value).toHaveLength(4);
|
||||
});
|
||||
it('"este_e_seguintes" disabled quando isFirstOccurrence', () => {
|
||||
const serieEvents = ref([{ recurrence_date: '2026-05-15' }, { recurrence_date: '2026-05-22' }]);
|
||||
const { composer } = setup(
|
||||
{ eventRow: { id: 'evt-1', recurrence_id: 'r-1', recurrence_date: '2026-05-15' } },
|
||||
{ serieEvents }
|
||||
);
|
||||
const opt = composer.editScopeOptions.value.find((o) => o.value === 'este_e_seguintes');
|
||||
expect(opt.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('qtdSessoesEfetiva', () => {
|
||||
it('valores fixos pelo mode', () => {
|
||||
const { composer } = setup();
|
||||
composer.qtdSessoesMode.value = '4';
|
||||
expect(composer.qtdSessoesEfetiva.value).toBe(4);
|
||||
composer.qtdSessoesMode.value = '8';
|
||||
expect(composer.qtdSessoesEfetiva.value).toBe(8);
|
||||
composer.qtdSessoesMode.value = '12';
|
||||
expect(composer.qtdSessoesEfetiva.value).toBe(12);
|
||||
});
|
||||
it('"personalizar" usa qtdSessoesCustom', () => {
|
||||
const { composer } = setup();
|
||||
composer.qtdSessoesMode.value = 'personalizar';
|
||||
composer.qtdSessoesCustom.value = 24;
|
||||
expect(composer.qtdSessoesEfetiva.value).toBe(24);
|
||||
});
|
||||
it('clampa em 1 quando custom é 0/null', () => {
|
||||
const { composer } = setup();
|
||||
composer.qtdSessoesMode.value = 'personalizar';
|
||||
composer.qtdSessoesCustom.value = 0;
|
||||
expect(composer.qtdSessoesEfetiva.value).toBe(1);
|
||||
composer.qtdSessoesCustom.value = null;
|
||||
expect(composer.qtdSessoesEfetiva.value).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('proximasOcorrencias', () => {
|
||||
function asISODate(d) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
it('retorna [] quando avulsa', () => {
|
||||
const { composer } = setup();
|
||||
composer.recorrenciaType.value = 'avulsa';
|
||||
composer.form.value.dia = new Date('2026-05-15');
|
||||
expect(composer.proximasOcorrencias.value).toEqual([]);
|
||||
});
|
||||
|
||||
it('semanal: 4 datas separadas por 7 dias', () => {
|
||||
const { composer } = setup();
|
||||
composer.recorrenciaType.value = 'semanal';
|
||||
composer.qtdSessoesMode.value = '4';
|
||||
composer.form.value.dia = new Date('2026-05-15T10:00:00');
|
||||
const list = composer.proximasOcorrencias.value;
|
||||
expect(list).toHaveLength(4);
|
||||
expect(asISODate(list[0])).toBe('2026-05-15');
|
||||
expect(asISODate(list[1])).toBe('2026-05-22');
|
||||
expect(asISODate(list[2])).toBe('2026-05-29');
|
||||
expect(asISODate(list[3])).toBe('2026-06-05');
|
||||
});
|
||||
|
||||
it('quinzenal: separação 14 dias', () => {
|
||||
const { composer } = setup();
|
||||
composer.recorrenciaType.value = 'quinzenal';
|
||||
composer.qtdSessoesMode.value = '4';
|
||||
composer.form.value.dia = new Date('2026-05-15T10:00:00');
|
||||
const list = composer.proximasOcorrencias.value;
|
||||
expect(list).toHaveLength(4);
|
||||
expect(asISODate(list[1])).toBe('2026-05-29');
|
||||
expect(asISODate(list[2])).toBe('2026-06-12');
|
||||
});
|
||||
|
||||
it('diasEspecificos: respeita dias selecionados', () => {
|
||||
const { composer } = setup();
|
||||
composer.recorrenciaType.value = 'diasEspecificos';
|
||||
composer.qtdSessoesMode.value = '4';
|
||||
composer.diasSelecionados.value = [1, 3]; // segunda + quarta
|
||||
composer.form.value.dia = new Date('2026-05-18T10:00:00'); // segunda
|
||||
const list = composer.proximasOcorrencias.value;
|
||||
expect(list).toHaveLength(4);
|
||||
// 18 (seg) - 20 (qua) - 25 (seg) - 27 (qua)
|
||||
expect(list[0].getDay()).toBe(1);
|
||||
expect(list[1].getDay()).toBe(3);
|
||||
expect(list[2].getDay()).toBe(1);
|
||||
expect(list[3].getDay()).toBe(3);
|
||||
});
|
||||
|
||||
it('diasEspecificos com array vazio retorna []', () => {
|
||||
const { composer } = setup();
|
||||
composer.recorrenciaType.value = 'diasEspecificos';
|
||||
composer.diasSelecionados.value = [];
|
||||
composer.form.value.dia = new Date('2026-05-15');
|
||||
expect(composer.proximasOcorrencias.value).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleDiaSelecionado', () => {
|
||||
it('adiciona se não existe, remove se existe', () => {
|
||||
const { composer } = setup();
|
||||
composer.toggleDiaSelecionado(1);
|
||||
expect(composer.diasSelecionados.value).toEqual([1]);
|
||||
composer.toggleDiaSelecionado(3);
|
||||
expect(composer.diasSelecionados.value).toEqual([1, 3]);
|
||||
composer.toggleDiaSelecionado(1);
|
||||
expect(composer.diasSelecionados.value).toEqual([3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isForaDoPlano (com dataLimiteManual)', () => {
|
||||
it('false quando dataLimiteManual null', () => {
|
||||
const { composer } = setup();
|
||||
expect(composer.isForaDoPlano(new Date('2026-12-31'))).toBe(false);
|
||||
});
|
||||
it('true quando data > limite', () => {
|
||||
const { composer } = setup();
|
||||
composer.dataLimiteManual.value = '2026-06-30';
|
||||
expect(composer.isForaDoPlano(new Date('2026-12-31'))).toBe(true);
|
||||
expect(composer.isForaDoPlano(new Date('2026-06-29'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitmentCards', () => {
|
||||
it('coloca native_key="session" primeiro', () => {
|
||||
const { composer } = setup({
|
||||
commitmentOptions: [MEETING_COMMITMENT, SESSION_COMMITMENT, { id: 'c-3', name: 'Avaliação' }]
|
||||
});
|
||||
const cards = composer.commitmentCards.value;
|
||||
// commitmentOptions vem via reactive(props) — itens viram proxies,
|
||||
// por isso comparamos id (toBe) em vez de identidade do objeto.
|
||||
expect(cards[0].id).toBe('c-session');
|
||||
});
|
||||
it('ordem alfabética entre não-session', () => {
|
||||
const { composer } = setup({
|
||||
commitmentOptions: [
|
||||
{ id: 'c-1', name: 'Zeta' },
|
||||
{ id: 'c-2', name: 'Alpha' },
|
||||
{ id: 'c-3', name: 'Beta' }
|
||||
]
|
||||
});
|
||||
const cards = composer.commitmentCards.value;
|
||||
expect(cards.map((c) => c.name)).toEqual(['Alpha', 'Beta', 'Zeta']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectedCommitment + relacionados', () => {
|
||||
it('selectedCommitment encontra pelo form.commitment_id', () => {
|
||||
const { composer } = setup({
|
||||
commitmentOptions: [SESSION_COMMITMENT, MEETING_COMMITMENT]
|
||||
});
|
||||
composer.form.value.commitment_id = 'c-meeting';
|
||||
expect(composer.selectedCommitment.value?.id).toBe('c-meeting');
|
||||
});
|
||||
it('selectedCommitmentName fallback "—" quando null', () => {
|
||||
const { composer } = setup();
|
||||
expect(composer.selectedCommitmentName.value).toBe('—');
|
||||
});
|
||||
it('requiresPatient true quando native_key=session', () => {
|
||||
const { composer } = setup({ commitmentOptions: [SESSION_COMMITMENT] });
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
expect(composer.requiresPatient.value).toBe(true);
|
||||
expect(composer.isSessionEvent.value).toBe(true);
|
||||
});
|
||||
it('requiresPatient false pra outros commitments', () => {
|
||||
const { composer } = setup({ commitmentOptions: [MEETING_COMMITMENT] });
|
||||
composer.form.value.commitment_id = 'c-meeting';
|
||||
expect(composer.requiresPatient.value).toBe(false);
|
||||
});
|
||||
it('patientLocked true só na edição de sessão com paciente', () => {
|
||||
const { composer } = setup({
|
||||
commitmentOptions: [SESSION_COMMITMENT],
|
||||
eventRow: { id: 'evt-1', paciente_id: 'p-1' }
|
||||
});
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
expect(composer.patientLocked.value).toBe(true);
|
||||
});
|
||||
it('hasInsurance reage a form.insurance_plan_id', () => {
|
||||
const { composer } = setup();
|
||||
expect(composer.hasInsurance.value).toBe(false);
|
||||
composer.form.value.insurance_plan_id = 'plan-1';
|
||||
expect(composer.hasInsurance.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('agendaPerms', () => {
|
||||
it('Inativo bloqueia create', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.paciente_status = 'Inativo';
|
||||
expect(composer.agendaPerms.value.canCreateSession).toBe(false);
|
||||
});
|
||||
it('Ativo permite tudo', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.paciente_status = 'Ativo';
|
||||
expect(composer.agendaPerms.value.canCreateSession).toBe(true);
|
||||
expect(composer.agendaPerms.value.canCreateRecurrence).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSessionFuture / isArchivedPastEdit / isInativoFutureEdit', () => {
|
||||
let mockNow;
|
||||
beforeEach(() => {
|
||||
mockNow = new Date('2026-05-15T12:00:00').getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockNow);
|
||||
});
|
||||
|
||||
it('isSessionFuture true quando criando (não-edit)', () => {
|
||||
const { composer } = setup({ eventRow: null });
|
||||
expect(composer.isSessionFuture.value).toBe(true);
|
||||
});
|
||||
it('isSessionFuture true quando edit + sessão futura', () => {
|
||||
const { composer } = setup({
|
||||
eventRow: { id: 'evt-1', inicio_em: '2026-05-20T10:00:00' }
|
||||
});
|
||||
expect(composer.isSessionFuture.value).toBe(true);
|
||||
});
|
||||
it('isSessionFuture false quando edit + sessão passada', () => {
|
||||
const { composer } = setup({
|
||||
eventRow: { id: 'evt-1', inicio_em: '2026-05-10T10:00:00' }
|
||||
});
|
||||
expect(composer.isSessionFuture.value).toBe(false);
|
||||
});
|
||||
it('isArchivedPastEdit true: edit + Arquivado + passada', () => {
|
||||
const { composer } = setup({
|
||||
eventRow: { id: 'evt-1', inicio_em: '2026-05-10T10:00:00' }
|
||||
});
|
||||
composer.form.value.paciente_status = 'Arquivado';
|
||||
expect(composer.isArchivedPastEdit.value).toBe(true);
|
||||
});
|
||||
it('isInativoFutureEdit true: edit + Inativo + futura', () => {
|
||||
const { composer } = setup({
|
||||
eventRow: { id: 'evt-1', inicio_em: '2026-05-20T10:00:00' }
|
||||
});
|
||||
composer.form.value.paciente_status = 'Inativo';
|
||||
expect(composer.isInativoFutureEdit.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inicioDateTime / fimDateTime', () => {
|
||||
it('null quando dia ou startTime ausentes', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.dia = null;
|
||||
expect(composer.inicioDateTime.value).toBe(null);
|
||||
});
|
||||
it('combina dia + startTime corretamente', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.dia = new Date('2026-05-15T00:00:00');
|
||||
composer.form.value.startTime = '14:30';
|
||||
const ini = composer.inicioDateTime.value;
|
||||
expect(ini.getHours()).toBe(14);
|
||||
expect(ini.getMinutes()).toBe(30);
|
||||
});
|
||||
it('fimDateTime adiciona duracaoMin', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.dia = new Date('2026-05-15T00:00:00');
|
||||
composer.form.value.startTime = '14:00';
|
||||
composer.form.value.duracaoMin = 50;
|
||||
const fim = composer.fimDateTime.value;
|
||||
expect(fim.getHours()).toBe(14);
|
||||
expect(fim.getMinutes()).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startTimeDate (computed get/set)', () => {
|
||||
it('getter null quando startTime null', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.startTime = null;
|
||||
expect(composer.startTimeDate.value).toBe(null);
|
||||
});
|
||||
it('getter retorna Date com hora setada', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.startTime = '09:30';
|
||||
const d = composer.startTimeDate.value;
|
||||
expect(d.getHours()).toBe(9);
|
||||
expect(d.getMinutes()).toBe(30);
|
||||
});
|
||||
it('setter atualiza form.startTime no formato HH:MM', () => {
|
||||
const { composer } = setup();
|
||||
const d = new Date();
|
||||
d.setHours(15, 5, 0, 0);
|
||||
composer.startTimeDate.value = d;
|
||||
expect(composer.form.value.startTime).toBe('15:05');
|
||||
});
|
||||
it('setter null limpa form.startTime', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.startTime = '14:00';
|
||||
composer.startTimeDate.value = null;
|
||||
expect(composer.form.value.startTime).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canSave (matriz de validação — núcleo)', () => {
|
||||
function ready(composer) {
|
||||
composer.form.value.owner_id = 'owner-1';
|
||||
composer.form.value.dia = new Date('2026-05-15');
|
||||
composer.form.value.startTime = '14:00';
|
||||
composer.form.value.commitment_id = 'c-meeting';
|
||||
composer.form.value.duracaoMin = 50;
|
||||
}
|
||||
|
||||
it('false sem owner_id', () => {
|
||||
const { composer } = setup({ commitmentOptions: [MEETING_COMMITMENT] });
|
||||
ready(composer);
|
||||
composer.form.value.owner_id = '';
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('false sem dia', () => {
|
||||
const { composer } = setup({ commitmentOptions: [MEETING_COMMITMENT] });
|
||||
ready(composer);
|
||||
composer.form.value.dia = null;
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('false sem startTime', () => {
|
||||
const { composer } = setup({ commitmentOptions: [MEETING_COMMITMENT] });
|
||||
ready(composer);
|
||||
composer.form.value.startTime = null;
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('false ao criar sem commitment_id', () => {
|
||||
const { composer } = setup({ commitmentOptions: [MEETING_COMMITMENT] });
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = null;
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('true ao EDITAR sem commitment_id (sessões antigas)', () => {
|
||||
const { composer } = setup({
|
||||
commitmentOptions: [MEETING_COMMITMENT],
|
||||
eventRow: { id: 'evt-1', inicio_em: '2026-05-15T14:00:00' }
|
||||
});
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = null; // legacy null
|
||||
expect(composer.canSave.value).toBe(true);
|
||||
});
|
||||
it('false em sessão sem paciente_id quando requiresPatient', () => {
|
||||
const { composer } = setup({ commitmentOptions: [SESSION_COMMITMENT] });
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
composer.form.value.paciente_id = null;
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('false em sessão particular sem itens de billing', () => {
|
||||
const items = ref([]);
|
||||
const { composer } = setup(
|
||||
{ commitmentOptions: [SESSION_COMMITMENT] },
|
||||
{ commitmentItems: items }
|
||||
);
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
composer.form.value.paciente_id = 'p-1';
|
||||
composer.billingType.value = 'particular';
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('true em sessão particular COM itens', () => {
|
||||
const items = ref([{ id: 'i-1' }]);
|
||||
const { composer } = setup(
|
||||
{ commitmentOptions: [SESSION_COMMITMENT] },
|
||||
{ commitmentItems: items }
|
||||
);
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
composer.form.value.paciente_id = 'p-1';
|
||||
composer.billingType.value = 'particular';
|
||||
expect(composer.canSave.value).toBe(true);
|
||||
});
|
||||
it('false ao criar sessão pra paciente Inativo', () => {
|
||||
const items = ref([{ id: 'i-1' }]);
|
||||
const { composer } = setup(
|
||||
{ commitmentOptions: [SESSION_COMMITMENT] },
|
||||
{ commitmentItems: items }
|
||||
);
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
composer.form.value.paciente_id = 'p-1';
|
||||
composer.form.value.paciente_status = 'Inativo';
|
||||
composer.billingType.value = 'particular';
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('false ao criar recorrência pra paciente Arquivado', () => {
|
||||
const items = ref([{ id: 'i-1' }]);
|
||||
const { composer } = setup(
|
||||
{ commitmentOptions: [SESSION_COMMITMENT] },
|
||||
{ commitmentItems: items }
|
||||
);
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
composer.form.value.paciente_id = 'p-1';
|
||||
composer.form.value.paciente_status = 'Arquivado';
|
||||
composer.billingType.value = 'particular';
|
||||
composer.recorrenciaType.value = 'semanal';
|
||||
expect(composer.canSave.value).toBe(false);
|
||||
});
|
||||
it('true em billing convenio sem precisar de itens', () => {
|
||||
const items = ref([]);
|
||||
const { composer } = setup(
|
||||
{ commitmentOptions: [SESSION_COMMITMENT] },
|
||||
{ commitmentItems: items }
|
||||
);
|
||||
ready(composer);
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
composer.form.value.paciente_id = 'p-1';
|
||||
composer.billingType.value = 'convenio';
|
||||
expect(composer.canSave.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeConflict (pré-check antes do PATCH)', () => {
|
||||
function readyForm(composer) {
|
||||
composer.form.value.dia = new Date('2026-05-15T00:00:00');
|
||||
composer.form.value.startTime = '14:00';
|
||||
composer.form.value.duracaoMin = 50;
|
||||
}
|
||||
|
||||
it('null quando form ainda incompleto', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.dia = null;
|
||||
expect(composer.timeConflict.value).toBe(null);
|
||||
});
|
||||
it('null sem allEvents', () => {
|
||||
const { composer } = setup({ allEvents: [] });
|
||||
readyForm(composer);
|
||||
expect(composer.timeConflict.value).toBe(null);
|
||||
});
|
||||
it('detecta overlap com evento existente', () => {
|
||||
const { composer } = setup({
|
||||
allEvents: [{
|
||||
id: 'evt-x',
|
||||
inicio_em: '2026-05-15T14:30:00',
|
||||
fim_em: '2026-05-15T15:20:00',
|
||||
paciente_nome: 'Maria'
|
||||
}]
|
||||
});
|
||||
readyForm(composer);
|
||||
expect(composer.timeConflict.value).toMatch(/Maria/);
|
||||
});
|
||||
it('NÃO detecta overlap com o próprio evento (form.id === evt.id)', () => {
|
||||
const { composer } = setup({
|
||||
allEvents: [{
|
||||
id: 'self',
|
||||
inicio_em: '2026-05-15T14:00:00',
|
||||
fim_em: '2026-05-15T14:50:00'
|
||||
}]
|
||||
});
|
||||
readyForm(composer);
|
||||
composer.form.value.id = 'self';
|
||||
expect(composer.timeConflict.value).toBe(null);
|
||||
});
|
||||
it('null quando evento adjacente (sem overlap real)', () => {
|
||||
const { composer } = setup({
|
||||
allEvents: [{
|
||||
id: 'evt-x',
|
||||
inicio_em: '2026-05-15T15:00:00',
|
||||
fim_em: '2026-05-15T15:50:00'
|
||||
}]
|
||||
});
|
||||
readyForm(composer);
|
||||
composer.form.value.duracaoMin = 50; // 14:00–14:50
|
||||
expect(composer.timeConflict.value).toBe(null);
|
||||
});
|
||||
it('detecta sobreposição com pausa do dia', () => {
|
||||
const { composer } = setup({
|
||||
pausasSemanais: [{
|
||||
dia_semana: 5, // sexta (2026-05-15)
|
||||
hora_inicio: '14:30',
|
||||
hora_fim: '15:00'
|
||||
}]
|
||||
});
|
||||
readyForm(composer);
|
||||
expect(composer.timeConflict.value).toMatch(/pausa/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('totalConflitos / sessoesForaDoPlano', () => {
|
||||
it('totalConflitos conta ocorrências com folga/feriado/bloqueado/pausa', () => {
|
||||
const { composer } = setup({
|
||||
workRules: [
|
||||
{ dia_semana: 1 }, { dia_semana: 2 }, { dia_semana: 3 },
|
||||
{ dia_semana: 4 }, { dia_semana: 5 } // seg-sex
|
||||
],
|
||||
blockedDates: ['2026-05-22']
|
||||
});
|
||||
composer.recorrenciaType.value = 'semanal';
|
||||
composer.qtdSessoesMode.value = '4';
|
||||
composer.form.value.dia = new Date('2026-05-15T10:00:00'); // sexta
|
||||
// 15 (sex ok) - 22 (sex bloqueada) - 29 (sex ok) - 5/jun (sex ok)
|
||||
expect(composer.totalConflitos.value).toBe(1);
|
||||
});
|
||||
it('sessoesForaDoPlano com dataLimiteManual', () => {
|
||||
const { composer } = setup();
|
||||
composer.recorrenciaType.value = 'semanal';
|
||||
composer.qtdSessoesMode.value = '4';
|
||||
composer.form.value.dia = new Date('2026-05-15T10:00:00');
|
||||
composer.dataLimiteManual.value = '2026-05-22';
|
||||
// ocorrências carregam hora 10:00 (de form.dia); limite "2026-05-22"
|
||||
// vira meia-noite local. Logo 22/5 10:00 > 22/5 00:00 → fora.
|
||||
// Resultado: 15 (dentro), 22 (fora), 29 (fora), 5/jun (fora) = 3
|
||||
expect(composer.sessoesForaDoPlano.value).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('headerTitle', () => {
|
||||
it('"Editar compromisso" quando isEdit', () => {
|
||||
const { composer } = setup({ eventRow: { id: 'evt-1' } });
|
||||
expect(composer.headerTitle.value).toBe('Editar compromisso');
|
||||
});
|
||||
it('"Novo compromisso — escolha o tipo" no step 1', () => {
|
||||
const { composer } = setup();
|
||||
composer.step.value = 1;
|
||||
expect(composer.headerTitle.value).toMatch(/escolha o tipo/);
|
||||
});
|
||||
it('"Novo compromisso" no step 2', () => {
|
||||
const { composer } = setup();
|
||||
composer.step.value = 2;
|
||||
expect(composer.headerTitle.value).toBe('Novo compromisso');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computedTitulo', () => {
|
||||
it('usa titulo_custom quando preenchido', () => {
|
||||
const { composer } = setup();
|
||||
composer.form.value.titulo_custom = 'Minha custom';
|
||||
expect(composer.computedTitulo.value).toBe('Minha custom');
|
||||
});
|
||||
it('"—" quando não há commitment selecionado (selectedCommitmentName fallback)', () => {
|
||||
// selectedCommitmentName retorna "—" quando sem commitment, então
|
||||
// computedTitulo herda esse "—" porque "—" || "Compromisso" → "—".
|
||||
// Comportamento original do .vue preservado.
|
||||
const { composer } = setup();
|
||||
expect(composer.computedTitulo.value).toBe('—');
|
||||
});
|
||||
it('combina nome paciente + commitment quando session', () => {
|
||||
const { composer } = setup({ commitmentOptions: [SESSION_COMMITMENT] });
|
||||
composer.form.value.commitment_id = 'c-session';
|
||||
composer.form.value.paciente_nome = 'Ana';
|
||||
expect(composer.computedTitulo.value).toBe('Ana [Sessão]');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user