// #region MaintenanceFormTests [C:2] [TYPE Module] [SEMANTICS test, component, maintenance, start, form, environment_id] // @BRIEF Component tests for StartMaintenanceForm — verifies environment_id is included in submission. // @RELATION BINDS_TO -> [StartMaintenanceForm] // @TEST_CONTRACT: Submit includes environment_id from environmentContextStore // @TEST_EDGE: No environment selected -> error toast, no API call // @TEST_EDGE: Environment selected -> API call includes environment_id import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; // ── Hoisted mutable state (before vi.mock calls, hoisted to top) ── const { mockStartMaintenance, mockAddToast, mockEnvStore } = vi.hoisted(() => { // Mutable env store state — tests can change this before render let _selectedEnvId = 'env-prod'; const mockEnvStoreObj = { value: { get selectedEnvId() { return _selectedEnvId; }, }, subscribe: vi.fn(), }; return { mockStartMaintenance: vi.fn(), mockAddToast: vi.fn(), mockEnvStore: { obj: mockEnvStoreObj, setSelectedEnvId(id: string) { _selectedEnvId = id; }, }, }; }); // ── Module mocks ────────────────────────────────────────────── vi.mock('$lib/api/maintenance.js', () => ({ startMaintenance: mockStartMaintenance, })); vi.mock('$lib/toasts.svelte.js', () => ({ addToast: mockAddToast, })); vi.mock('$lib/stores/environmentContext.svelte.js', () => ({ environmentContextStore: mockEnvStore.obj, })); vi.mock('$lib/i18n/index.svelte.js', () => { const subscribers = new Set(); const tData = { maintenance: { start_maintenance: 'Start Maintenance', start_maintenance_description: 'Schedule a maintenance window', start_now: 'Start Maintenance', starting: 'Starting...', start_success: 'Maintenance started. Task: {task_id}', start_error: 'Failed to start maintenance', table_name: 'Table Name', table_required: 'Table name is required', table_name_placeholder: 'e.g. raw.sales', table_help: 'Enter the table name', start_time_label: 'Start Time', start_time_help: 'When maintenance begins', end_time_label: 'End Time (optional)', end_time_help: 'When maintenance ends', message_label: 'Message (optional)', message_placeholder: 'Scheduled data mart refresh', template_label: 'Quick Template', template_none: 'Custom', template_daily_refresh: 'Daily Refresh', template_daily_refresh_desc: '4h ETL window', template_schema_migration: 'Schema Migration', template_schema_migration_desc: '2h schema change', template_emergency: 'Emergency', template_emergency_desc: '1h critical fix', template_extended: 'Extended', template_extended_desc: '8h large refresh', no_environment: 'No environment selected. Please select an environment first.', }, common: {}, }; return { t: { subscribe(run: (_value: unknown) => void) { subscribers.add(run); run(tData); return () => subscribers.delete(run); }, }, _: (key: string) => key, getT: () => ({}), }; }); // ── Imports (after mocks) ───────────────────────────────────── import StartMaintenanceForm from '$lib/components/StartMaintenanceForm.svelte'; describe('StartMaintenanceForm', () => { beforeEach(() => { vi.clearAllMocks(); // Reset to default: environment selected mockEnvStore.setSelectedEnvId('env-prod'); }); // #region test_submits_with_environment_id [C:2] [TYPE Function] // @BRIEF Submit with environment selected includes environment_id in API call. // @TEST_EDGE: Environment selected -> API call includes environment_id it('submits with environment_id from context store', async () => { mockStartMaintenance.mockResolvedValue({ task_id: 'task-1', maintenance_id: 'm-1', status: 'pending', }); render(StartMaintenanceForm); // Fill in table name const tableInput = screen.getByPlaceholderText('e.g. raw.sales'); await fireEvent.input(tableInput, { target: { value: 'raw.my_table' } }); // Click the submit button (use role to avoid heading match) const submitButton = screen.getByRole('button', { name: /Start Maintenance/i }); await fireEvent.click(submitButton); // Verify startMaintenance was called with environment_id expect(mockStartMaintenance).toHaveBeenCalledTimes(1); const callParams = mockStartMaintenance.mock.calls[0][0]; expect(callParams).toHaveProperty('environment_id', 'env-prod'); expect(callParams).toHaveProperty('tables'); expect(callParams.tables).toContain('raw.my_table'); }); // #endregion test_submits_with_environment_id // #region test_validation_fails_without_environment [C:2] [TYPE Function] // @BRIEF No environment selected shows error toast and does not call API. // @TEST_EDGE: No environment selected -> error toast, no API call it('shows error toast when no environment is selected', async () => { // Set empty environment mockEnvStore.setSelectedEnvId(''); render(StartMaintenanceForm); // Fill in table name const tableInput = screen.getByPlaceholderText('e.g. raw.sales'); await fireEvent.input(tableInput, { target: { value: 'raw.my_table' } }); // Click the submit button const submitButton = screen.getByRole('button', { name: /Start Maintenance/i }); await fireEvent.click(submitButton); // API should NOT have been called expect(mockStartMaintenance).not.toHaveBeenCalled(); // Error toast should have been shown with the missing environment message expect(mockAddToast).toHaveBeenCalledWith( expect.stringContaining('No environment selected'), 'error', ); }); // #endregion test_validation_fails_without_environment // #region test_validation_fails_without_table [C:2] [TYPE Function] // @BRIEF Empty table name shows inline validation, no API call. it('shows validation error when table name is empty', async () => { render(StartMaintenanceForm); // Click Submit without filling table name const submitButton = screen.getByRole('button', { name: /Start Maintenance/i }); await fireEvent.click(submitButton); // API should NOT have been called expect(mockStartMaintenance).not.toHaveBeenCalled(); // Inline validation error should be visible expect(screen.getByText('Table name is required')).toBeTruthy(); }); // #endregion test_validation_fails_without_table }); // #endregion MaintenanceFormTests