181 lines
7.2 KiB
TypeScript
181 lines
7.2 KiB
TypeScript
// #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
|