Skip to content

Commit 7ca1bb4

Browse files
committed
add coverage reporting and some more tests
1 parent 19db75b commit 7ca1bb4

File tree

7 files changed

+280
-3
lines changed

7 files changed

+280
-3
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ exampleVault/.obsidian/*
3434
exampleVault/.obsidian/plugins/*
3535
exampleVault/.obsidian/plugins/obsidian-meta-bind-plugin/*
3636
!exampleVault/.obsidian/plugins/obsidian-meta-bind-plugin/.hotreload
37+
38+
coverage/

bunfig.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
[test]
22
preload = ["./tests/__preload__/happydom.ts", "./tests/__preload__/svelteLoader.ts", "./tests/__preload__/obsidianMock.ts", "./tests/__preload__/customMatchers.ts"]
3-
root = "./tests"
3+
root = "./tests"
4+
coverage = true
5+
coverageReporter = ["text", "lcov"]

packages/core/src/api/SyntaxHighlightingAPI.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
HLP_ViewFieldDeclaration,
1313
} from 'packages/core/src/parsers/syntaxHighlighting/HLPs';
1414
import { SyntaxHighlighting } from 'packages/core/src/parsers/syntaxHighlighting/SyntaxHighlighting';
15+
import { expectType } from '../utils/Utils';
1516

1617
export class SyntaxHighlightingAPI {
1718
public readonly plugin: IPlugin;
@@ -41,6 +42,8 @@ export class SyntaxHighlightingAPI {
4142
return this.highlightInlineButtonDeclaration(str, trimWhiteSpace);
4243
}
4344

45+
expectType<never>(inlineFieldType);
46+
4447
throw new Error(`Unknown MDRCType ${inlineFieldType}`);
4548
}
4649

packages/core/src/parsers/ParsingError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Parser } from '@lemons_dev/parsinom/lib/Parser';
33
import { ErrorLevel, ErrorType, MetaBindError } from 'packages/core/src/utils/errors/MetaBindErrors';
44

55
export function runParser<T>(parser: Parser<T>, str: string): T {
6-
const result = parser.tryParse(str);
6+
const result = parser.thenEof().tryParse(str);
77
if (result.success) {
88
return result.value;
99
} else {

tests/__mocks__/TestComponent.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export class TestComponent {
2+
loaded: boolean;
3+
used: boolean;
4+
callbacks: (() => void)[];
5+
6+
constructor() {
7+
this.loaded = false;
8+
this.used = false;
9+
this.callbacks = [];
10+
}
11+
12+
load() {
13+
if (this.loaded) {
14+
throw new Error('Attempted double load of TestComponent');
15+
}
16+
if (this.used) {
17+
throw new Error('Attempted reuse of TestComponent');
18+
}
19+
this.loaded = true;
20+
}
21+
22+
unload() {
23+
if (!this.loaded && !this.used) {
24+
throw new Error('Attempted unload of TestComponent before it was loaded');
25+
}
26+
if (!this.loaded && this.used) {
27+
throw new Error('Attempted double unload of TestComponent');
28+
}
29+
this.loaded = false;
30+
this.used = true;
31+
this.callbacks.forEach(cb => cb());
32+
}
33+
34+
register(callback: () => void) {
35+
if (this.used) {
36+
throw new Error('Attempted to register callback after TestComponent was used. The callback will never be called.');
37+
}
38+
39+
this.callbacks.push(callback);
40+
}
41+
}

tests/__mocks__/TestInternalAPI.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { ModalContent } from 'packages/core/src/modals/ModalContent';
1212
import type { IModal } from 'packages/core/src/modals/IModal';
1313
import { SelectModalContent } from 'packages/core/src/modals/SelectModalContent';
1414
import type { ContextMenuItemDefinition, IContextMenu } from 'packages/core/src/utils/IContextMenu';
15-
import { TestFileSystem } from 'tests/__mocks__/TestFileSystem';
1615
import YAML from 'yaml';
1716
import { z, ZodType } from 'zod';
1817
import type { LifecycleHook } from 'packages/core/src/api/API';

tests/api.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { beforeEach, describe, expect, mock, type Mock, spyOn, test } from 'bun:test';
2+
import { TestPlugin } from './__mocks__/TestPlugin';
3+
import { InputFieldMountable } from 'packages/core/src/fields/inputFields/InputFieldMountable';
4+
import { ViewFieldMountable } from 'packages/core/src/fields/viewFields/ViewFieldMountable';
5+
import { JsViewFieldMountable } from 'packages/core/src/fields/viewFields/JsViewFieldMountable';
6+
import { TableMountable } from 'packages/core/src/fields/metaBindTable/TableMountable';
7+
import { ButtonGroupMountable } from 'packages/core/src/fields/button/ButtonGroupMountable';
8+
import { ButtonMountable } from 'packages/core/src/fields/button/ButtonMountable';
9+
import { EmbedMountable } from 'packages/core/src/fields/embed/EmbedMountable';
10+
import { ExcludedMountable } from 'packages/core/src/fields/excluded/ExcludedMountable';
11+
import { FieldType, RenderChildType } from 'packages/core/src/config/APIConfigs';
12+
import { PropPath } from 'packages/core/src/utils/prop/PropPath';
13+
import { PropAccess, PropAccessType } from 'packages/core/src/utils/prop/PropAccess';
14+
import { TestComponent } from './__mocks__/TestComponent';
15+
16+
describe('api', () => {
17+
let plugin = new TestPlugin();
18+
19+
beforeEach(() => {
20+
plugin = new TestPlugin();
21+
});
22+
23+
describe('createField', () => {
24+
test('input field', () => {
25+
let field = plugin.api.createField(FieldType.INPUT, '', {
26+
declaration: 'INPUT[toggle:foo]',
27+
renderChildType: RenderChildType.BLOCK,
28+
}, true);
29+
30+
expect(field).toBeInstanceOf(InputFieldMountable);
31+
});
32+
33+
test('view field', () => {
34+
let field = plugin.api.createField(FieldType.VIEW, '', {
35+
declaration: 'VIEW[1 + {foo}]',
36+
renderChildType: RenderChildType.BLOCK,
37+
}, true);
38+
39+
expect(field).toBeInstanceOf(ViewFieldMountable);
40+
});
41+
42+
test('js view field', () => {
43+
let field = plugin.api.createField(FieldType.JS_VIEW, '', {
44+
declaration: '{foo} as foo\n---\nreturn 1 + context.bound.foo',
45+
}, true);
46+
47+
expect(field).toBeInstanceOf(JsViewFieldMountable);
48+
});
49+
50+
test('table field', () => {
51+
let field = plugin.api.createField(FieldType.TABLE, '', {
52+
bindTarget: plugin.api.parseBindTarget('foo', ''),
53+
columns: [],
54+
tableHead: [],
55+
}, true);
56+
57+
expect(field).toBeInstanceOf(TableMountable);
58+
});
59+
60+
test('button group field', () => {
61+
let field = plugin.api.createField(FieldType.BUTTON_GROUP, '', {
62+
declaration: 'BUTTON[foo]',
63+
renderChildType: RenderChildType.BLOCK,
64+
}, true);
65+
66+
expect(field).toBeInstanceOf(ButtonGroupMountable);
67+
});
68+
69+
test('button field', () => {
70+
let field = plugin.api.createField(FieldType.BUTTON, '', {
71+
declaration: `style: primary
72+
label: Open Meta Bind Playground
73+
class: green-button
74+
action:
75+
type: command
76+
command: obsidian-meta-bind-plugin:open-playground`,
77+
isPreview: false,
78+
}, true);
79+
80+
expect(field).toBeInstanceOf(ButtonMountable);
81+
});
82+
83+
test('embed field', () => {
84+
let field = plugin.api.createField(FieldType.EMBED, '', {
85+
content: '[[some note]]',
86+
depth: 1,
87+
}, true);
88+
89+
expect(field).toBeInstanceOf(EmbedMountable);
90+
});
91+
92+
test('excluded field', () => {
93+
let field = plugin.api.createField(FieldType.EXCLUDED, '', undefined, true);
94+
95+
expect(field).toBeInstanceOf(ExcludedMountable);
96+
});
97+
});
98+
99+
describe('createInlineFieldFromString', () => {
100+
test('input field', () => {
101+
let field = plugin.api.createInlineFieldFromString('INPUT[toggle:foo]', '', undefined);
102+
103+
expect(field).toBeInstanceOf(InputFieldMountable);
104+
});
105+
106+
test('view field', () => {
107+
let field = plugin.api.createInlineFieldFromString('VIEW[1 + {foo}]', '', undefined);
108+
109+
expect(field).toBeInstanceOf(ViewFieldMountable);
110+
});
111+
112+
test('button group field', () => {
113+
let field = plugin.api.createInlineFieldFromString('BUTTON[foo]', '', undefined);
114+
115+
expect(field).toBeInstanceOf(ButtonGroupMountable);
116+
});
117+
});
118+
119+
describe('isInlineFieldDeclaration', () => {
120+
test('input field', () => {
121+
expect(plugin.api.isInlineFieldDeclaration(FieldType.INPUT, 'INPUT[toggle:foo]')).toBe(true);
122+
});
123+
124+
test('view field', () => {
125+
expect(plugin.api.isInlineFieldDeclaration(FieldType.VIEW, 'VIEW[1 + {foo}]')).toBe(true);
126+
});
127+
128+
test('button group field', () => {
129+
expect(plugin.api.isInlineFieldDeclaration(FieldType.BUTTON_GROUP, 'BUTTON[foo]')).toBe(true);
130+
});
131+
132+
test('not a field', () => {
133+
expect(plugin.api.isInlineFieldDeclaration(FieldType.INPUT, 'foo')).toBe(false);
134+
});
135+
136+
test('wrong field', () => {
137+
expect(plugin.api.isInlineFieldDeclaration(FieldType.INPUT, 'VIEW[1 + {foo}]')).toBe(false);
138+
});
139+
});
140+
141+
describe('createBindTarget', () => {
142+
test('simple bind target', () => {
143+
let bindTarget = plugin.api.createBindTarget('frontmatter', 'file', ['foo']);
144+
145+
expect(bindTarget).toEqual({
146+
storageType: 'frontmatter',
147+
storagePath: 'file',
148+
storageProp: new PropPath([ new PropAccess(PropAccessType.OBJECT, 'foo') ]),
149+
listenToChildren: false,
150+
});
151+
});
152+
153+
test('nested bind target', () => {
154+
let bindTarget = plugin.api.createBindTarget('frontmatter', 'file', ['foo', '0', 'bar']);
155+
156+
expect(bindTarget).toEqual({
157+
storageType: 'frontmatter',
158+
storagePath: 'file',
159+
storageProp: new PropPath([
160+
new PropAccess(PropAccessType.OBJECT, 'foo'),
161+
new PropAccess(PropAccessType.ARRAY, '0'),
162+
new PropAccess(PropAccessType.OBJECT, 'bar'),
163+
]),
164+
listenToChildren: false,
165+
});
166+
});
167+
});
168+
169+
describe('metadata update methods', () => {
170+
let bindTargetA = plugin.api.parseBindTarget('file#foo', '');
171+
let bindTargetB = plugin.api.parseBindTarget('file#bar', '');
172+
173+
test.each(['string', 5, false, ['array']])('setting a value, then reading it reads the same value', (value) => {
174+
plugin.api.setMetadata(bindTargetA, value);
175+
176+
expect(plugin.api.getMetadata(bindTargetA)).toEqual(value);
177+
});
178+
179+
test('setting metadata on different bind target does not affect the first one', () => {
180+
plugin.api.setMetadata(bindTargetA, 'foo');
181+
plugin.api.setMetadata(bindTargetB, 'bar');
182+
183+
expect(plugin.api.getMetadata(bindTargetA)).toEqual('foo');
184+
});
185+
186+
test('update callback works correctly', () => {
187+
plugin.api.setMetadata(bindTargetA, 0);
188+
189+
expect(plugin.api.getMetadata(bindTargetA)).toEqual(0);
190+
191+
plugin.api.updateMetadata(bindTargetA, (value) => value as number + 1);
192+
193+
expect(plugin.api.getMetadata(bindTargetA)).toEqual(1);
194+
});
195+
196+
test('subscribing to metadata changes works correctly', () => {
197+
plugin.api.setMetadata(bindTargetA, 0);
198+
199+
let lifecycle = new TestComponent();
200+
lifecycle.load();
201+
let callback = mock(() => {});
202+
plugin.api.subscribeToMetadata(bindTargetA, lifecycle, callback);
203+
204+
plugin.api.setMetadata(bindTargetA, 1);
205+
206+
// two times because the callback is called once when subscribing
207+
expect(callback).toHaveBeenCalledTimes(2);
208+
expect(callback).toHaveBeenLastCalledWith(1);
209+
});
210+
211+
test('unsubscribing from metadata works correctly', () => {
212+
plugin.api.setMetadata(bindTargetA, 0);
213+
214+
let lifecycle = new TestComponent();
215+
lifecycle.load();
216+
let callback = mock(() => {});
217+
plugin.api.subscribeToMetadata(bindTargetA, lifecycle, callback);
218+
219+
plugin.api.setMetadata(bindTargetA, 1);
220+
221+
lifecycle.unload();
222+
223+
plugin.api.setMetadata(bindTargetA, 2);
224+
225+
// two times because the callback is called once when subscribing
226+
expect(callback).toHaveBeenCalledTimes(2);
227+
expect(callback).toHaveBeenLastCalledWith(1);
228+
});
229+
});
230+
});

0 commit comments

Comments
 (0)