diff --git a/__tests__/ui/components/validation/validators.test.ts b/__tests__/ui/components/validation/validators.test.ts new file mode 100644 index 0000000..d22c1a8 --- /dev/null +++ b/__tests__/ui/components/validation/validators.test.ts @@ -0,0 +1,70 @@ +import { validateValueIsBoolean } from "src/ui/validation/validators"; + + +// Группа тестов для всего файла валидаторов +describe('Validation Utility Functions', () => { + + // Подгруппа тестов для конкретной функции validateValueIsBoolean + describe('validateValueIsBoolean', () => { + + // Тест кейсы для валидных значений (должны возвращать null) + test('should return null for true', () => { + expect(validateValueIsBoolean(true)).toBeNull(); + }); + + test('should return null for false', () => { + expect(validateValueIsBoolean(false)).toBeNull(); + }); + + // Тест кейсы для невалидных значений (должны возвращать объект ошибки) + test('should return error object for null', () => { + const expectedError = { message: 'Value must be a boolean.' }; + expect(validateValueIsBoolean(null)).toEqual(expectedError); + }); + + test('should return error object for undefined', () => { + const expectedError = { message: 'Value must be a boolean.' }; + expect(validateValueIsBoolean(undefined)).toEqual(expectedError); + }); + + test('should return error object for numbers', () => { + const expectedError = { message: 'Value must be a boolean.' }; + expect(validateValueIsBoolean(0)).toEqual(expectedError); + expect(validateValueIsBoolean(1)).toEqual(expectedError); + expect(validateValueIsBoolean(123)).toEqual(expectedError); + expect(validateValueIsBoolean(-10)).toEqual(expectedError); + }); + + test('should return error object for strings', () => { + const expectedError = { message: 'Value must be a boolean.' }; + expect(validateValueIsBoolean('')).toEqual(expectedError); + expect(validateValueIsBoolean('true')).toEqual(expectedError); + expect(validateValueIsBoolean('false')).toEqual(expectedError); + expect(validateValueIsBoolean('any string')).toEqual(expectedError); + }); + + test('should return error object for objects', () => { + const expectedError = { message: 'Value must be a boolean.' }; + expect(validateValueIsBoolean({})).toEqual(expectedError); + expect(validateValueIsBoolean({ a: 1 })).toEqual(expectedError); + }); + + test('should return error object for arrays', () => { + const expectedError = { message: 'Value must be a boolean.' }; + expect(validateValueIsBoolean([])).toEqual(expectedError); + expect(validateValueIsBoolean([true])).toEqual(expectedError); + }); + }); + + // --- Сюда можно будет добавлять describe-блоки для других функций валидации --- + /* + describe('validateJsonStringArray', () => { + // ... тесты для валидации JSON ... + }); + + describe('validateNotEmptyArray', () => { + // ... тесты для проверки на пустой массив ... + }); + */ + +}); \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index 048dde7..38d873b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -36,6 +36,12 @@ const config: Config = { // Limit the number of workers that run tests in parallel maxWorkers: 1, // <--- Задает последовательное выполнение + moduleNameMapper: { + // Этот паттерн говорит: если импорт начинается с 'src/', + // замени 'src/' на '/src/' при поиске файла. + '^src/(.*)$': '/src/$1', + }, + }; export default config; diff --git a/src/ui/components/BaseSettingComponent.ts b/src/ui/components/BaseSettingComponent.ts index fb319ad..ec138d3 100644 --- a/src/ui/components/BaseSettingComponent.ts +++ b/src/ui/components/BaseSettingComponent.ts @@ -1,5 +1,5 @@ import { Setting } from 'obsidian'; -import { CheckboxSyncPluginSettings } from '../../types'; +import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from '../../types'; import { ISettingComponent } from '../interfaces/ISettingComponent'; import { ValidationError } from '../validation/types'; // Убрали App и CheckboxSyncPlugin из импортов, если они не нужны ВСЕМ наследникам @@ -13,13 +13,26 @@ export abstract class BaseSettingComponent implements ISettingComponent { // --- Методы для реализации наследниками --- abstract getSettingKey(): keyof CheckboxSyncPluginSettings; - abstract getDefaultValue(): any; // render теперь должен сам создавать Setting и сохранять ссылку в this.setting abstract render(container: HTMLElement, currentValue: any): void; abstract getValueFromUi(): any; abstract setValueInUi(value: any): void; - abstract validate(value: any): ValidationError | null; + abstract validate(): ValidationError | null; + + public getDefaultValue(): any { + const key = this.getSettingKey(); // Получаем ключ от наследника + if (key in DEFAULT_SETTINGS) { + // Возвращаем значение из импортированного объекта + return DEFAULT_SETTINGS[key]; + } else { + // Логируем предупреждение, если ключ не найден (ошибка конфигурации) + console.warn( + `[${this.constructor.name}] Default value for key "${key}" not found in DEFAULT_SETTINGS. Returning undefined.` + ); + return undefined; // Или null, или выбросить ошибку, но undefined безопаснее + } + } // --- Общая реализация --- public setChangeListener(listener: () => void): void { this.onChangeCallback = listener; diff --git a/src/ui/components/EnableParentSyncSettingComponent.ts b/src/ui/components/EnableParentSyncSettingComponent.ts new file mode 100644 index 0000000..02b8e8a --- /dev/null +++ b/src/ui/components/EnableParentSyncSettingComponent.ts @@ -0,0 +1,70 @@ +import { Setting, ToggleComponent } from 'obsidian'; +import { CheckboxSyncPluginSettings } from '../../types'; +import { BaseSettingComponent } from './BaseSettingComponent'; +import { ValidationError } from '../validation/types'; +import { validateValueIsBoolean } from '../validation/validators'; + +export class EnableParentSyncSettingComponent extends BaseSettingComponent { + private toggleComponent: ToggleComponent; + + constructor() { + super(); + } + + getSettingKey(): keyof CheckboxSyncPluginSettings { + return 'enableAutomaticParentState'; // Ключ этой настройки + } + + render(container: HTMLElement, currentValue: any): void { + + this.setting = new Setting(container) + .setName('Update parent checkbox state automatically') + .setDesc('If enabled, the state of a parent checkbox will be automatically updated based on the state of its children (all children checked = parent checked). If disabled, only manually changing a parent will affect its children.') + .addToggle(toggle => { + this.toggleComponent = toggle; + // Используем as boolean, т.к. знаем тип для этой настройки + toggle + .setValue(currentValue as boolean) + .onChange(this.onChangeCallback); // Просто передаем колбэк + }); + } + + getValueFromUi(): boolean { + if (this.toggleComponent) { + return this.toggleComponent.getValue(); + } + console.warn(`getValueFromUi called before render for ${this.getSettingKey()}`); + // Используем унаследованный getDefaultValue() для консистентности + return this.getDefaultValue(); + } + + setValueInUi(value: any): void { + if (this.toggleComponent) { + this.toggleComponent.setValue(value as boolean); + } + } + + validate(): ValidationError | null { + let valueFromUi: boolean; + try { + valueFromUi = this.getValueFromUi(); + } catch (error) { + console.error(`Error getting value from UI for ${this.getSettingKey()}:`, error); + return { + field: this.getSettingKey(), + message: `Internal error getting value from UI: ${error instanceof Error ? error.message : String(error)}` + }; + } + + // Вызываем внешнюю чистую функцию валидации + const validationResult = validateValueIsBoolean(valueFromUi); + + if (validationResult) { + return { + field: this.getSettingKey(), + message: validationResult.message + }; + } + return null; + } +} \ No newline at end of file diff --git a/src/ui/interfaces/ISettingComponent.ts b/src/ui/interfaces/ISettingComponent.ts index 15ab5a4..9d26ce5 100644 --- a/src/ui/interfaces/ISettingComponent.ts +++ b/src/ui/interfaces/ISettingComponent.ts @@ -47,7 +47,7 @@ export interface ISettingComponent { * @param value - Чистое значение для валидации (обычно результат getValueFromUi). * @returns Объект ValidationError, если значение невалидно, иначе null. */ - validate(value: any): ValidationError | null; + validate(): ValidationError | null; /** * Устанавливает колбэк, который будет вызван при изменении значения в UI. diff --git a/src/ui/validation/validators.ts b/src/ui/validation/validators.ts new file mode 100644 index 0000000..2257793 --- /dev/null +++ b/src/ui/validation/validators.ts @@ -0,0 +1,49 @@ +// src/settings/validation/validators.ts + +/** + * Проверяет, является ли предоставленное значение булевым типом (boolean). + * Эта функция не зависит от контекста Obsidian или конкретного компонента. + * + * @param value - Значение любого типа для проверки. + * @returns Объект с сообщением об ошибке, если значение не является boolean, иначе null. + */ +export function validateValueIsBoolean(value: any): { message: string } | null { + if (typeof value !== 'boolean') { + return { message: 'Value must be a boolean.' }; + } + return null; +} + +// --- Сюда в будущем можно будет добавлять другие чистые функции валидации --- + +/* +// Пример для будущей валидации JSON-массива строк +export function validateJsonStringArray(rawJson: string): { message: string } | null { + try { + const parsed = JSON.parse(rawJson); + if (!Array.isArray(parsed)) { + return { message: 'Input must be a valid JSON array (e.g., ["x", " "]).' }; + } + for (let i = 0; i < parsed.length; i++) { + const element = parsed[i]; + if (typeof element !== 'string') { + return { message: `Array element at index ${i} is not a string.` }; + } + if (element.length !== 1) { + return { message: `Element "${element}" at index ${i} must be a single character.` }; + } + } + return null; // Все проверки пройдены + } catch (e: any) { + return { message: `Invalid JSON format: ${e.message}` }; + } +} + +// Пример для проверки, что массив не пустой +export function validateNotEmptyArray(arr: any[]): { message: string } | null { + if (!Array.isArray(arr) || arr.length === 0) { + return { message: 'The list cannot be empty.' }; + } + return null; +} +*/ \ No newline at end of file