mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 05:37:48 +00:00
add tests
This commit is contained in:
parent
892ce8c482
commit
55f74a8e60
12 changed files with 6187 additions and 2479 deletions
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
|
|
@ -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
37
.github/workflows/test.yml
vendored
Normal 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
2
.gitignore
vendored
|
|
@ -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/
|
||||||
|
|
|
||||||
64
__tests__/findCheckboxesLine.test.ts
Normal file
64
__tests__/findCheckboxesLine.test.ts
Normal 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" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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
38
jest.config.ts
Normal 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
55
main.ts
|
|
@ -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
8315
package-lock.json
generated
File diff suppressed because it is too large
Load diff
51
package.json
51
package.json
|
|
@ -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
44
src/checkboxUtils.ts
Normal 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
18
src/main.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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__
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue