test: expand coverage and report it in CI

- Add @vitest/coverage-v8, test:coverage script, vitest.config.ts
- Extract looksLikeTable to src/detect.ts so it is testable without
  pulling the obsidian runtime
- Add edge-case tests for parser, formatter, convert, and detect
  (23 -> 38 tests; line coverage 100% on every file except main.ts)
- Run coverage in CI and append the line % to the step summary
This commit is contained in:
Vesa Vänskä 2026-05-25 10:27:46 +03:00
parent 4a2f3d1d1c
commit 0c18d9123b
No known key found for this signature in database
11 changed files with 448 additions and 9 deletions

View file

@ -16,4 +16,14 @@ jobs:
cache: npm
- run: npm ci
- run: npm run build
- run: npm test
- run: npm run test:coverage
- name: Coverage summary
if: always()
run: |
if [ -f coverage/coverage-summary.json ]; then
LINE=$(jq -r '.total.lines.pct' coverage/coverage-summary.json)
echo "## Code Coverage" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Line coverage: ${LINE}%**" >> $GITHUB_STEP_SUMMARY
fi

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ node_modules
main.js
*.js.map
data.json
coverage

244
package-lock.json generated
View file

@ -9,12 +9,73 @@
"version": "1.0.1",
"devDependencies": {
"@types/node": "^20.11.0",
"@vitest/coverage-v8": "^4.1.7",
"esbuild": "^0.28.0",
"obsidian": "*",
"obsidian": "latest",
"typescript": "^5.3.0",
"vitest": "^4.1.7"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
@ -516,6 +577,16 @@
"node": ">=18"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@ -523,6 +594,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
@ -897,6 +979,37 @@
"@types/estree": "*"
}
},
"node_modules/@vitest/coverage-v8": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz",
"integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.7",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"magicast": "^0.5.2",
"obug": "^2.1.1",
"std-env": "^4.0.0-rc.1",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "4.1.7",
"vitest": "4.1.7"
},
"peerDependenciesMeta": {
"@vitest/browser": {
"optional": true
}
}
},
"node_modules/@vitest/expect": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
@ -1020,6 +1133,18 @@
"node": ">=12"
}
},
"node_modules/ast-v8-to-istanbul": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
"integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.31",
"estree-walker": "^3.0.3",
"js-tokens": "^10.0.0"
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@ -1157,6 +1282,69 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@ -1428,6 +1616,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/magicast": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.3",
"@babel/types": "^7.29.0",
"source-map-js": "^1.2.1"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
@ -1573,6 +1789,19 @@
"@rolldown/binding-win32-x64-msvc": "1.0.2"
}
},
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@ -1612,6 +1841,19 @@
"license": "MIT",
"peer": true
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",

View file

@ -7,10 +7,12 @@
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@vitest/coverage-v8": "^4.1.7",
"esbuild": "^0.28.0",
"obsidian": "latest",
"typescript": "^5.3.0",

View file

@ -82,4 +82,49 @@ describe("convertSelection", () => {
expect(editor.getValue()).toBe(doc);
});
it("does nothing when the selection is empty", () => {
const doc = "│ A │ B │\n│ 1 │ 2 │";
const editor = new FakeEditor(doc, { line: 0, ch: 5 }, { line: 0, ch: 5 });
convertSelection(editor as unknown as Editor);
expect(editor.getValue()).toBe(doc);
});
it("trims the trailing line when the selection ends at column 0 of the next line", () => {
// Mimics a triple-click drag: selection extends through "\n" onto a new line.
const doc =
"┌──────┬──────┐\n" +
"│ A │ B │\n" +
"├──────┼──────┤\n" +
"│ 1 │ 2 │\n" +
"└──────┴──────┘\n" +
"after";
const editor = new FakeEditor(doc, { line: 0, ch: 0 }, { line: 5, ch: 0 });
convertSelection(editor as unknown as Editor);
expect(editor.getValue()).toBe(
"| A | B |\n" + "| - | - |\n" + "| 1 | 2 |\n" + "after"
);
});
it("preserves text before and after a table embedded mid-document", () => {
const doc =
"before\n" +
"┌──────┬──────┐\n" +
"│ A │ B │\n" +
"├──────┼──────┤\n" +
"│ 1 │ 2 │\n" +
"└──────┴──────┘\n" +
"after";
const editor = new FakeEditor(doc, { line: 1, ch: 0 }, { line: 5, ch: 15 });
convertSelection(editor as unknown as Editor);
expect(editor.getValue()).toBe(
"before\n" + "| A | B |\n" + "| - | - |\n" + "| 1 | 2 |\n" + "after"
);
});
});

View file

@ -0,0 +1,26 @@
import { describe, it, expect } from "vitest";
import { looksLikeTable } from "../detect";
describe("looksLikeTable", () => {
it("detects Unicode box-drawing characters", () => {
expect(looksLikeTable("│ a │ b │")).toBe(true);
expect(looksLikeTable("┌──┬──┐")).toBe(true);
expect(looksLikeTable("└──┴──┘")).toBe(true);
expect(looksLikeTable("├──┼──┤")).toBe(true);
});
it("detects ASCII borders with dashes and equals", () => {
expect(looksLikeTable("+----+----+")).toBe(true);
expect(looksLikeTable("+====+====+")).toBe(true);
});
it("rejects plain prose", () => {
expect(looksLikeTable("Just some regular text.")).toBe(false);
expect(looksLikeTable("Two\nlines, no table.")).toBe(false);
});
it("rejects a plain Markdown table without box-drawing or +-+ borders", () => {
const md = "| a | b |\n| - | - |\n| 1 | 2 |";
expect(looksLikeTable(md)).toBe(false);
});
});

View file

@ -30,6 +30,11 @@ describe("looksLikeIdentifier", () => {
expect(looksLikeIdentifier("admin")).toBe(false);
expect(looksLikeIdentifier("hello")).toBe(false);
});
it("detects colon-separated identifiers", () => {
expect(looksLikeIdentifier("Foo:Bar")).toBe(true);
expect(looksLikeIdentifier("namespace:type")).toBe(true);
});
});
describe("formatMarkdownTable", () => {
@ -99,6 +104,30 @@ describe("formatMarkdownTable", () => {
expect(result).toContain("fixed via ShowableScope");
});
it("does not wrap header values in backticks even when they look like identifiers", () => {
const table: ParsedTable = {
headers: ["MyClass", "Note"],
rows: [["plain", "text"]],
};
const result = formatMarkdownTable(table);
const headerLine = result.split("\n")[0];
expect(headerLine).toContain("MyClass");
expect(headerLine).not.toContain("`MyClass`");
});
it("formats a single-row table", () => {
const table: ParsedTable = {
headers: ["A", "B"],
rows: [["1", "2"]],
};
expect(formatMarkdownTable(table)).toBe(
"| A | B |\n| - | - |\n| 1 | 2 |"
);
});
it("handles empty cells", () => {
const table: ParsedTable = {
headers: ["A", "B"],

View file

@ -131,6 +131,75 @@ describe("parseTable", () => {
]);
});
it("parses an ASCII table with = borders", () => {
const input = `+======+======+
| Name | Age |
+======+======+
| Eve | 22 |
+======+======+`;
const result = parseTable(input);
expect(result).not.toBeNull();
expect(result!.headers).toEqual(["Name", "Age"]);
expect(result!.rows).toEqual([["Eve", "22"]]);
});
it("parses an existing Markdown pipe table", () => {
const input = `| Name | Age |
| ---- | --- |
| Dana | 31 |
| Erik | 28 |`;
const result = parseTable(input);
expect(result).not.toBeNull();
expect(result!.headers).toEqual(["Name", "Age"]);
expect(result!.rows).toEqual([
["Dana", "31"],
["Erik", "28"],
]);
});
it("drops rows whose column count differs from the header", () => {
const input = `│ A │ B │
1 2
x y z
3 4 `;
const result = parseTable(input);
expect(result).not.toBeNull();
expect(result!.headers).toEqual(["A", "B"]);
expect(result!.rows).toEqual([
["1", "2"],
["3", "4"],
]);
});
it("handles mixed │ and | delimiters across lines", () => {
const input = `│ A │ B │
| 1 | 2 |`;
const result = parseTable(input);
expect(result).not.toBeNull();
expect(result!.headers).toEqual(["A", "B"]);
expect(result!.rows).toEqual([["1", "2"]]);
});
it("tolerates blank lines between data rows when no border is present", () => {
const input = `│ A │ B │
1 2 `;
const result = parseTable(input);
expect(result).not.toBeNull();
expect(result!.headers).toEqual(["A", "B"]);
expect(result!.rows).toEqual([["1", "2"]]);
});
it("handles a three-column table", () => {
const input = `┌──────┬──────┬──────┐
A B C

8
src/detect.ts Normal file
View file

@ -0,0 +1,8 @@
const TABLE_HINT = /[│┌┬┐├┼┤└┴┘─]/;
// Quick heuristic gate used by main.ts before the more expensive parseTable.
// True for ASCII box-drawing tables and +---+ / +===+ ruled tables; false for
// plain prose and plain Markdown pipe tables (which need no conversion).
export function looksLikeTable(text: string): boolean {
return TABLE_HINT.test(text) || /\+[-=]+\+/.test(text);
}

View file

@ -1,11 +1,6 @@
import { Editor, Menu, Plugin } from "obsidian";
import { convertSelection } from "./convert";
const TABLE_HINT = /[│┌┬┐├┼┤└┴┘─]/;
function looksLikeTable(text: string): boolean {
return TABLE_HINT.test(text) || /\+[-=]+\+/.test(text);
}
import { looksLikeTable } from "./detect";
export default class TableBeautifierPlugin extends Plugin {
onload() {

12
vitest.config.ts Normal file
View file

@ -0,0 +1,12 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
exclude: ["src/**/__tests__/**"],
reporter: ["text", "html", "json-summary"],
},
},
});