Skip to content

Commit 1bdb748

Browse files
committed
test: cover slug/smart casing and case suggestions
1 parent 6100e88 commit 1bdb748

File tree

4 files changed

+154
-3
lines changed

4 files changed

+154
-3
lines changed

src/formatters/formatter-case.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,13 @@ describe("Formatter case: pipe option", () => {
122122
expect(result).toBe("a=My New Blog b=my-new-blog c=MY NEW BLOG");
123123
});
124124

125-
it("supports snake/camel/pascal/title/lower transforms", async () => {
125+
it("supports snake/camel/pascal/title/lower/slug transforms", async () => {
126126
formatter.setValueResponse("my new blog");
127127
const result = await formatter.testFormat(
128-
"snake={{VALUE|case:snake}} camel={{VALUE|case:camel}} pascal={{VALUE|case:pascal}} title={{VALUE|case:title}} lower={{VALUE|case:lower}}",
128+
"snake={{VALUE|case:snake}} camel={{VALUE|case:camel}} pascal={{VALUE|case:pascal}} title={{VALUE|case:title}} lower={{VALUE|case:lower}} slug={{VALUE|case:slug}}",
129129
);
130130
expect(result).toBe(
131-
"snake=my_new_blog camel=myNewBlog pascal=MyNewBlog title=My New Blog lower=my new blog",
131+
"snake=my_new_blog camel=myNewBlog pascal=MyNewBlog title=My New Blog lower=my new blog slug=my-new-blog",
132132
);
133133
});
134134

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { describe, expect, it, beforeEach } from "vitest";
2+
import { FormatSyntaxSuggester } from "./formatSyntaxSuggester";
3+
4+
function ensureObsidianDomPolyfills(): void {
5+
(globalThis as any).createDiv ??= (cls?: string) => {
6+
const div = document.createElement("div");
7+
if (cls) div.className = cls;
8+
return div;
9+
};
10+
11+
const proto = HTMLElement.prototype as any;
12+
13+
proto.createDiv ??= function (arg?: string | { cls?: string }) {
14+
const div = document.createElement("div");
15+
if (typeof arg === "string") div.className = arg;
16+
else if (arg && typeof arg === "object" && typeof arg.cls === "string")
17+
div.className = arg.cls;
18+
this.appendChild(div);
19+
return div;
20+
};
21+
22+
proto.empty ??= function () {
23+
this.replaceChildren();
24+
return this;
25+
};
26+
27+
// Obsidian adds delegated event helpers; tests don't need behavior here.
28+
proto.on ??= function () {
29+
return this;
30+
};
31+
32+
proto.detach ??= function () {
33+
this.remove();
34+
};
35+
36+
proto.addClass ??= function (...classes: string[]) {
37+
this.classList.add(...classes);
38+
return this;
39+
};
40+
41+
proto.removeClass ??= function (...classes: string[]) {
42+
this.classList.remove(...classes);
43+
return this;
44+
};
45+
46+
proto.setAttr ??= function (name: string, value: string) {
47+
this.setAttribute(name, value);
48+
return this;
49+
};
50+
}
51+
52+
describe("FormatSyntaxSuggester case style suggestions", () => {
53+
beforeEach(() => {
54+
ensureObsidianDomPolyfills();
55+
});
56+
57+
it("suggests kebab when typing a |case: prefix", async () => {
58+
const app = {
59+
dom: { appContainerEl: document.body },
60+
keymap: { pushScope: () => {}, popScope: () => {} },
61+
} as any;
62+
const plugin = {
63+
settings: { choices: [], globalVariables: {} },
64+
getTemplateFiles: () => [],
65+
} as any;
66+
67+
const inputEl = document.createElement("input");
68+
inputEl.value = "{{VALUE:title|case:k";
69+
inputEl.selectionStart = inputEl.value.length;
70+
inputEl.selectionEnd = inputEl.value.length;
71+
72+
const suggester = new FormatSyntaxSuggester(app, inputEl, plugin);
73+
const suggestions = await suggester.getSuggestions(inputEl.value);
74+
expect(suggestions).toEqual(["kebab"]);
75+
suggester.destroy();
76+
});
77+
78+
it("suggests all styles (including slug) when case fragment is empty", async () => {
79+
const app = {
80+
dom: { appContainerEl: document.body },
81+
keymap: { pushScope: () => {}, popScope: () => {} },
82+
} as any;
83+
const plugin = {
84+
settings: { choices: [], globalVariables: {} },
85+
getTemplateFiles: () => [],
86+
} as any;
87+
88+
const inputEl = document.createElement("input");
89+
inputEl.value = "{{VALUE:title|case:";
90+
inputEl.selectionStart = inputEl.value.length;
91+
inputEl.selectionEnd = inputEl.value.length;
92+
93+
const suggester = new FormatSyntaxSuggester(app, inputEl, plugin);
94+
const suggestions = await suggester.getSuggestions(inputEl.value);
95+
expect(suggestions).toContain("slug");
96+
expect(suggestions).toContain("kebab");
97+
expect(suggestions).toContain("snake");
98+
expect(suggestions).toContain("camel");
99+
expect(suggestions).toContain("pascal");
100+
expect(suggestions).toContain("title");
101+
expect(suggestions).toContain("lower");
102+
expect(suggestions).toContain("upper");
103+
suggester.destroy();
104+
});
105+
});
106+

src/utils/caseTransform.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from "vitest";
2+
import { transformCase } from "./caseTransform";
3+
4+
describe("transformCase", () => {
5+
it("supports basic transforms", () => {
6+
const input = "My New Blog";
7+
expect(transformCase(input, "kebab")).toBe("my-new-blog");
8+
expect(transformCase(input, "snake")).toBe("my_new_blog");
9+
expect(transformCase(input, "camel")).toBe("myNewBlog");
10+
expect(transformCase(input, "pascal")).toBe("MyNewBlog");
11+
expect(transformCase(input, "title")).toBe("My New Blog");
12+
expect(transformCase(input, "lower")).toBe("my new blog");
13+
expect(transformCase(input, "upper")).toBe("MY NEW BLOG");
14+
});
15+
16+
it("handles acronyms with smart camel and preserves them for pascal/title", () => {
17+
expect(transformCase("API Key", "camel")).toBe("apiKey");
18+
expect(transformCase("My API Key", "camel")).toBe("myAPIKey");
19+
expect(transformCase("API Key", "pascal")).toBe("APIKey");
20+
expect(transformCase("API Key", "title")).toBe("API Key");
21+
});
22+
23+
it("preserves mixed-case brand tokens like iOS", () => {
24+
expect(transformCase("iOS App", "camel")).toBe("iOSApp");
25+
expect(transformCase("iOS App", "pascal")).toBe("iOSApp");
26+
expect(transformCase("iOS App", "title")).toBe("iOS App");
27+
});
28+
29+
it("supports slug and avoids reserved Windows device names", () => {
30+
expect(transformCase("My New Blog", "slug")).toBe("my-new-blog");
31+
expect(transformCase("CON", "slug")).toBe("con-");
32+
expect(transformCase("A/B*C?", "slug")).toBe("a-b-c");
33+
});
34+
});
35+

tests/obsidian-stub.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,15 @@ export function normalizePath(p: string): string {
511511
return p.replace(/\\/g, '/');
512512
}
513513

514+
// Minimal debounce for tests: execute immediately.
515+
export function debounce<T extends (...args: any[]) => any>(
516+
fn: T,
517+
_wait: number,
518+
_resetTimer?: boolean,
519+
): T {
520+
return fn;
521+
}
522+
514523
// Default export for compatibility
515524
export default {
516525
App,
@@ -534,4 +543,5 @@ export default {
534543
Notice,
535544
moment,
536545
normalizePath,
546+
debounce,
537547
};

0 commit comments

Comments
 (0)