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'
jobs:
build:
test:
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:
- name: Checkout repository
uses: actions/checkout@v4
@ -26,12 +49,12 @@ jobs:
- name: Build plugin
run: npm run build
- name: Create release
- name: Create release folder
run: |
mkdir release
cp main.js manifest.json styles.css release/
- name: Upload artifact
- name: Upload built artifact
uses: actions/upload-artifact@v4
with:
name: obsidian-plugin
@ -40,7 +63,6 @@ jobs:
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
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
.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: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ["./src/main.ts"],
bundle: true,
external: [
"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",
"version": "1.0.0",
"description": "Automatically checks the parent checkbox if all child checkboxes are completed, and unchecks it otherwise",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
"name": "checkbox-sync",
"version": "1.0.0",
"description": "Automatically checks the parent checkbox if all child checkboxes are completed, and unchecks it otherwise",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"prebuild": "npm test",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"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",
"ES6",
"ES7"
]
],
"esModuleInterop": true,
// "rootDir": "src", // Указываем, где исходный код
// "outDir": "dist", // Куда компилировать
"resolveJsonModule": true // Поддержка импорта JSON файлов
},
"include": [
"**/*.ts"
"**/*.ts",
"src/**/*.ts", // Включаем только исходники из папки src
"__tests__/**/*.ts" // Включаем тесты из папки __tests__
]
}