add tests

This commit is contained in:
Grol Grol 2025-02-18 20:50:31 +03:00
parent 892ce8c482
commit 55f74a8e60
12 changed files with 6187 additions and 2479 deletions

View file

@ -8,9 +8,32 @@ on:
- 'manifest.json' - 'manifest.json'
jobs: jobs:
build: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Upload test coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
build:
needs: test
runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -26,12 +49,12 @@ jobs:
- name: Build plugin - name: Build plugin
run: npm run build run: npm run build
- name: Create release - name: Create release folder
run: | run: |
mkdir release mkdir release
cp main.js manifest.json styles.css release/ cp main.js manifest.json styles.css release/
- name: Upload artifact - name: Upload built artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: obsidian-plugin name: obsidian-plugin
@ -40,7 +63,6 @@ jobs:
release: release:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4

37
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: Run Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- name: Checkout репозитория
uses: actions/checkout@v4
- name: Установка Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Установка зависимостей
run: npm ci
- name: Запуск тестов
run: npm test -- --coverage
- name: Сохранение отчёта о покрытии
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: coverage/

2
.gitignore vendored
View file

@ -20,3 +20,5 @@ data.json
# Exclude macOS Finder (System Explorer) View States # Exclude macOS Finder (System Explorer) View States
.DS_Store .DS_Store
coverage/

View file

@ -0,0 +1,64 @@
import { findCheckboxesLine, syncCheckboxes } from "../src/checkboxUtils";
describe("findCheckboxesLine", () => {
test("распознаёт корректные строки с чекбоксами", () => {
expect(findCheckboxesLine("- [ ] Task")).not.toBeNull();
expect(findCheckboxesLine(" - [ ] Task")).not.toBeNull();
expect(findCheckboxesLine("- [x] Task")).not.toBeNull();
expect(findCheckboxesLine("* [ ] Another task")).not.toBeNull();
expect(findCheckboxesLine("+ [x] Task")).not.toBeNull();
expect(findCheckboxesLine("1. [ ] Numbered task")).not.toBeNull();
expect(findCheckboxesLine("1. [x] Numbered task")).not.toBeNull();
});
test("игнорирует некорректные строки", () => {
// Отсутствует пробел после маркера списка
expect(findCheckboxesLine("-[ ] Task")).toBeNull();
expect(findCheckboxesLine("*[x] Task")).toBeNull();
expect(findCheckboxesLine("1.[ ] Numbered task")).toBeNull();
// Неверное содержимое скобок или неверный регистр
expect(findCheckboxesLine("- [] Task")).toBeNull();
expect(findCheckboxesLine("- [X] Task")).toBeNull();
// Не соответствует шаблону чекбокса
expect(findCheckboxesLine(" - Task")).toBeNull();
expect(findCheckboxesLine("Just some text")).toBeNull();
expect(findCheckboxesLine("1. Some numbered task")).toBeNull();
});
});
describe("syncCheckboxes", () => {
test("обновляет родительский чекбокс, если все дочерние отмечены", () => {
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Child 2";
// Для строки "- [ ] Parent":
// match[1] = "" (нулевой отступ), match[2] = "-" (1 символ)
// => позиция чекбокса = 0 + 1 + 2 = 3
expect(syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: "x" }]);
});
test("снимает отметку у родителя, если не все дочерние отмечены", () => {
const text = "- [x] Parent\n - [x] Child 1\n - [ ] Child 2";
// Родительский чекбокс должен стать незамеченным, то есть "x" -> " "
expect(syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: " " }]);
});
test("не изменяет родителя, если у него нет дочерних элементов", () => {
const text = "- [x] Single";
expect(syncCheckboxes(text)).toEqual([]);
});
test("тройная вложенность: обновляет только промежуточный уровень", () => {
const text = "- [ ] Parent\n - [ ] Child\n - [x] Grandchild";
// Для строки " - [ ] Child":
// match[1] = " " (2 пробела), match[2] = "-" (1 символ)
// => позиция чекбокса = 2 + 1 + 2 = 5
expect(syncCheckboxes(text)).toEqual([{ line: 1, ch: 5, value: "x" }]);
});
test("тройная вложенность: обновляет родительский чекбокс, если все вложенные отмечены", () => {
const text = "- [ ] Parent\n - [x] Child\n - [x] Grandchild";
// Здесь для строки " - [x] Child" изменений не требуется (она уже отмечена).
// Родительский элемент имеет единственного дочернего, отмеченного как [x],
// поэтому ожидается обновление строки Parent: позиция = 0 + 1 + 2 = 3.
expect(syncCheckboxes(text)).toEqual([{ line: 0, ch: 3, value: "x" }]);
});
});

View file

@ -15,7 +15,7 @@ const context = await esbuild.context({
banner: { banner: {
js: banner, js: banner,
}, },
entryPoints: ["main.ts"], entryPoints: ["./src/main.ts"],
bundle: true, bundle: true,
external: [ external: [
"obsidian", "obsidian",

38
jest.config.ts Normal file
View file

@ -0,0 +1,38 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type {Config} from 'jest';
const config: Config = {
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// An array of file extensions your modules use
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
preset: "ts-jest",
testEnvironment: "node",
};
export default config;

55
main.ts
View file

@ -1,55 +0,0 @@
import { Plugin, Editor } from "obsidian";
export default class CheckboxSyncPlugin extends Plugin {
async onload() {
this.registerEvent(
this.app.workspace.on("editor-change", (editor) => {
this.updateParentCheckboxes(editor);
})
);
}
updateParentCheckboxes(editor: Editor) {
const lines = editor.getValue().split("\n");
let updates: { line: number; ch: number; value: string }[] = [];
for (let i = lines.length - 1; i >= 0; i--) {
// Регулярные выражения для всех типов списков (с учетом пробела перед чекбоксом)
const match = lines[i].match(/^(\s*)([*+-]|\d+\.) \[([ x-])\] /);
if (!match) continue;
const indent = match[1].length; // Индентирование
const isChecked = match[3] === "x"; // Состояние чекбокса
let allChildrenChecked = true;
let hasChildren = false;
let j = i + 1;
// Ищем вложенные элементы для всех типов списков
while (j < lines.length) {
const childMatch = lines[j].match(/^(\s*)([*+-]|\d+\.) \[([ x-])\] /);
if (!childMatch || childMatch[1].length <= indent) break;
hasChildren = true;
if (childMatch[3] !== "x") allChildrenChecked = false;
j++;
}
// Обновляем родительский чекбокс, если есть дочерние
if (hasChildren) {
const checkboxPos = match[1].length + match[2].length + 2; // Позиция чекбокса
if (allChildrenChecked && !isChecked) {
updates.push({ line: i, ch: checkboxPos, value: "x" });
} else if (!allChildrenChecked && isChecked) {
updates.push({ line: i, ch: checkboxPos, value: " " });
}
}
}
// Применяем изменения через replaceRange()
if (updates.length > 0) {
editor.blur();
updates.forEach(({ line, ch, value }) => {
editor.replaceRange(value, { line, ch }, { line, ch: ch + 1 });
});
}
}
}

8315
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,31 @@
{ {
"name": "checkbox-sync", "name": "checkbox-sync",
"version": "1.0.0", "version": "1.0.0",
"description": "Automatically checks the parent checkbox if all child checkboxes are completed, and unchecks it otherwise", "description": "Automatically checks the parent checkbox if all child checkboxes are completed, and unchecks it otherwise",
"main": "main.js", "main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs", "scripts": {
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "dev": "node esbuild.config.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json" "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
}, "prebuild": "npm test",
"keywords": [], "version": "node version-bump.mjs && git add manifest.json versions.json",
"author": "", "test": "jest"
"license": "MIT", },
"devDependencies": { "keywords": [],
"@types/node": "^16.11.6", "author": "",
"@typescript-eslint/eslint-plugin": "5.29.0", "license": "MIT",
"@typescript-eslint/parser": "5.29.0", "devDependencies": {
"builtin-modules": "3.3.0", "@types/jest": "^29.5.14",
"esbuild": "0.17.3", "@types/node": "^16.11.6",
"obsidian": "latest", "@typescript-eslint/eslint-plugin": "5.29.0",
"tslib": "2.4.0", "@typescript-eslint/parser": "5.29.0",
"typescript": "4.7.4" "builtin-modules": "3.3.0",
} "esbuild": "^0.25.0",
"jest": "^29.7.0",
"obsidian": "^1.7.2",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
} }

44
src/checkboxUtils.ts Normal file
View file

@ -0,0 +1,44 @@
/**
* Функция для поиска строк с чекбоксами в формате Markdown.
*/
export function findCheckboxesLine(line: string): RegExpMatchArray | null {
return line.match(/^(\s*)([*+-]|\d+\.) \[([ x-])\] /);
}
/**
* Возвращает список изменений (позиции и новые значения), но не модифицирует текст напрямую.
*/
export function syncCheckboxes(text: string): { line: number; ch: number; value: string }[] {
const lines = text.split("\n");
let updates: { line: number; ch: number; value: string }[] = [];
for (let i = lines.length - 1; i >= 0; i--) {
const match = findCheckboxesLine(lines[i]);
if (!match) continue;
const indent = match[1].length;
const isChecked = match[3] === "x";
let allChildrenChecked = true;
let hasChildren = false;
let j = i + 1;
while (j < lines.length) {
const childMatch = findCheckboxesLine(lines[j]);
if (!childMatch || childMatch[1].length <= indent) break;
hasChildren = true;
if (childMatch[3] !== "x") allChildrenChecked = false;
j++;
}
if (hasChildren) {
const checkboxPos = match[1].length + match[2].length + 2;
if (allChildrenChecked && !isChecked) {
updates.push({ line: i, ch: checkboxPos, value: "x" });
} else if (!allChildrenChecked && isChecked) {
updates.push({ line: i, ch: checkboxPos, value: " " });
}
}
}
return updates;
}

18
src/main.ts Normal file
View file

@ -0,0 +1,18 @@
import { Plugin, Editor } from "obsidian";
import { syncCheckboxes } from "./checkboxUtils";
export default class CheckboxSyncPlugin extends Plugin {
async onload() {
this.registerEvent(
this.app.workspace.on("editor-change", (editor) => {
const updates = syncCheckboxes(editor.getValue());
if (updates.length > 0) {
editor.blur();
updates.forEach(({ line, ch, value }) => {
editor.replaceRange(value, { line, ch }, { line, ch: ch + 1 });
});
}
})
);
}
}

View file

@ -16,9 +16,15 @@
"ES5", "ES5",
"ES6", "ES6",
"ES7" "ES7"
] ],
"esModuleInterop": true,
// "rootDir": "src", // Указываем, где исходный код
// "outDir": "dist", // Куда компилировать
"resolveJsonModule": true // Поддержка импорта JSON файлов
}, },
"include": [ "include": [
"**/*.ts" "**/*.ts",
"src/**/*.ts", // Включаем только исходники из папки src
"__tests__/**/*.ts" // Включаем тесты из папки __tests__
] ]
} }