Skip to content

Commit c34d6a0

Browse files
authored
Merge pull request #3633 from obsidian-tasks-group/remove-mockedFileData-singleton
test: Remove uses of a singleton in mocked Obsidian functions
2 parents ba9b932 + f2c3dc7 commit c34d6a0

File tree

7 files changed

+215
-36
lines changed

7 files changed

+215
-36
lines changed

tests/Obsidian/SimulatedFile.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { CachedMetadata } from 'obsidian';
22
import type { Task } from 'Task/Task';
33
import { logging } from '../../src/lib/logging';
44
import { FileParser } from '../../src/Obsidian/FileParser';
5-
import { setCurrentCacheFile } from '../__mocks__/obsidian';
65
import { MockDataLoader } from '../TestingTools/MockDataLoader';
76
import type { MockDataName } from './AllCacheSampleData';
87

@@ -56,7 +55,6 @@ export interface SimulatedFile {
5655
export function readTasksFromSimulatedFile(filename: MockDataName): Task[] {
5756
const testData = MockDataLoader.get(filename);
5857
const logger = logging.getLogger('testCache');
59-
setCurrentCacheFile(testData);
6058
const fileParser = new FileParser(
6159
testData.filePath,
6260
testData.fileContents,

tests/Scripting/TasksFile.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { TasksFile } from '../../src/Scripting/TasksFile';
44
import { getTasksFileFromMockData, listPathAndData } from '../TestingTools/MockDataHelpers';
55
import { LinkResolver } from '../../src/Task/LinkResolver';
66
import type { MockDataName } from '../Obsidian/AllCacheSampleData';
7+
import { getAllTags, getFirstLinkpathDest, parseFrontMatterTags } from '../__mocks__/obsidian';
8+
import { MockDataLoader } from '../TestingTools/MockDataLoader';
79
import { determineExpressionType, formatToRepresentType } from './ScriptingTestHelpers';
810

911
afterEach(() => {
@@ -306,6 +308,58 @@ describe('TasksFile - reading tags', () => {
306308
const tasksFile = getTasksFileFromMockData(testDataName);
307309
expect(tasksFile.frontmatter.tags).toEqual([]);
308310
});
311+
312+
it('should be able to read all tags for any loaded SimulatedFile', () => {
313+
const file1 = getTasksFileFromMockData('yaml_tags_with_one_value_on_new_line');
314+
expect(getAllTags(file1.cachedMetadata)).toEqual(['#single-value-new-line', '#task']);
315+
316+
const file2 = getTasksFileFromMockData('yaml_tags_with_one_value_on_single_line');
317+
expect(getAllTags(file2.cachedMetadata)).toEqual(['#single-value-single-line', '#task']);
318+
319+
// Now see if we can again find the tags in file1
320+
expect(getAllTags(file1.cachedMetadata)).toEqual(['#single-value-new-line', '#task']);
321+
});
322+
323+
it('should be able to read frontmatter tags for any loaded SimulatedFile', () => {
324+
const file1 = getTasksFileFromMockData('yaml_tags_with_one_value_on_new_line');
325+
expect(parseFrontMatterTags(file1.cachedMetadata.frontmatter)).toEqual(['#single-value-new-line']);
326+
327+
const file2 = getTasksFileFromMockData('yaml_tags_with_one_value_on_single_line');
328+
expect(parseFrontMatterTags(file2.cachedMetadata.frontmatter)).toEqual(['#single-value-single-line']);
329+
330+
// Now see if we can again find the tags in file1
331+
expect(parseFrontMatterTags(file1.cachedMetadata.frontmatter)).toEqual(['#single-value-new-line']);
332+
333+
const t = () => {
334+
parseFrontMatterTags(file1.frontmatter);
335+
};
336+
expect(t).toThrow(Error);
337+
expect(t).toThrowError(
338+
'FrontMatterCache not found in any loaded SimulatedFile. Did you supply TasksFile.frontmatter instead of TasksFile.cachedMetadata.frontmatter?',
339+
);
340+
});
341+
342+
it('should be able to call getFirstLinkpathDest() for any loaded SimulatedFile', () => {
343+
function loadMockDataAndResolveFirstLink(testDataName: MockDataName, expectedLinkSource: string) {
344+
const file = getTasksFileFromMockData(testDataName);
345+
const link = file.cachedMetadata.links![0];
346+
expect(link.original).toMatchInlineSnapshot(expectedLinkSource);
347+
const markdownPath = MockDataLoader.markdownPath(testDataName);
348+
const firstLinkpathDest = getFirstLinkpathDest(link, markdownPath);
349+
return { link, markdownPath, firstLinkpathDest };
350+
}
351+
352+
const destination1 = loadMockDataAndResolveFirstLink('link_in_file_body', '"[[yaml_tags_is_empty]]"');
353+
expect(destination1.firstLinkpathDest).toMatchInlineSnapshot('"Test Data/yaml_tags_is_empty.md"');
354+
355+
const destination2 = loadMockDataAndResolveFirstLink('link_in_heading', '"[[multiple_headings]]"');
356+
expect(destination2.firstLinkpathDest).toMatchInlineSnapshot('"Test Data/multiple_headings.md"');
357+
358+
// Now see if we can again resolve the link from file1
359+
expect(getFirstLinkpathDest(destination1.link, destination1.markdownPath)).toMatchInlineSnapshot(
360+
'"Test Data/yaml_tags_is_empty.md"',
361+
);
362+
});
309363
});
310364

311365
describe('TasksFile - properties', () => {

tests/TestingTools/MockDataHelpers.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { setCurrentCacheFile } from '../__mocks__/obsidian';
21
import { TasksFile } from '../../src/Scripting/TasksFile';
32
import type { SimulatedFile } from '../Obsidian/SimulatedFile';
43
import type { MockDataName } from '../Obsidian/AllCacheSampleData';
@@ -27,7 +26,6 @@ import { MockDataLoader } from './MockDataLoader';
2726
*/
2827
export function getTasksFileFromMockData(testDataName: MockDataName) {
2928
const data = MockDataLoader.get(testDataName);
30-
setCurrentCacheFile(data);
3129
const cachedMetadata = data.cachedMetadata;
3230
return new TasksFile(data.filePath, cachedMetadata);
3331
}

tests/TestingTools/MockDataLoader.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,59 @@ describe('MockDataLoader', () => {
2727
const path = MockDataLoader.markdownPath('query_using_properties');
2828
expect(path).toEqual('Test Data/query_using_properties.md');
2929
});
30+
31+
it('should locate loaded SimulatedFile from its CachedMetadata', () => {
32+
const data1 = MockDataLoader.get('yaml_tags_has_multiple_values');
33+
const data2 = MockDataLoader.get('yaml_tags_with_two_values_on_two_lines');
34+
35+
expect(MockDataLoader.findCachedMetaData(data1.cachedMetadata)).toBe(data1);
36+
expect(MockDataLoader.findCachedMetaData(data2.cachedMetadata)).toBe(data2);
37+
});
38+
39+
it('should detect CachedMetadata not previously loaded, even if it is a clone of a loaded file', () => {
40+
const data1 = MockDataLoader.get('one_task');
41+
42+
// typescript:S7784 Prefer `structuredClone(…)` over `JSON.parse(JSON.stringify(…))` to create a deep clone.
43+
// But structuredClone() is not available in Jest.
44+
const clonedMetadata = JSON.parse(JSON.stringify(data1.cachedMetadata)); // NOSONAR
45+
expect(clonedMetadata).toStrictEqual(data1.cachedMetadata);
46+
const t = () => {
47+
MockDataLoader.findCachedMetaData(clonedMetadata);
48+
};
49+
50+
expect(t).toThrow(Error);
51+
expect(t).toThrowError('CachedMetadata not found in any loaded SimulatedFile');
52+
});
53+
54+
it('should locate loaded SimulatedFile from its Frontmatter', () => {
55+
const data1 = MockDataLoader.get('yaml_tags_has_multiple_values');
56+
const data2 = MockDataLoader.get('yaml_tags_with_two_values_on_two_lines');
57+
58+
expect(MockDataLoader.findFrontmatter(data1.cachedMetadata.frontmatter)).toBe(data1);
59+
expect(MockDataLoader.findFrontmatter(data2.cachedMetadata.frontmatter)).toBe(data2);
60+
});
61+
62+
it('should detect call of findFrontmatter() with unknown Frontmatter', () => {
63+
const t = () => {
64+
MockDataLoader.findFrontmatter({});
65+
};
66+
expect(t).toThrow(Error);
67+
expect(t).toThrowError('FrontMatterCache not found in any loaded SimulatedFile');
68+
});
69+
70+
it('should locate loaded SimulatedFile from its path', () => {
71+
const data1 = MockDataLoader.get('callout');
72+
const data2 = MockDataLoader.get('no_yaml');
73+
74+
expect(MockDataLoader.findDataFromMarkdownPath('Test Data/callout.md')).toBe(data1);
75+
expect(MockDataLoader.findDataFromMarkdownPath('Test Data/no_yaml.md')).toBe(data2);
76+
});
77+
78+
it('should detect call of findDataFromMarkdownPath() with unknown path', () => {
79+
const t = () => {
80+
MockDataLoader.findDataFromMarkdownPath('Test Data/non-existent path.md');
81+
};
82+
expect(t).toThrow(Error);
83+
expect(t).toThrowError('Markdown path not found in any loaded SimulatedFile');
84+
});
3085
});

tests/TestingTools/MockDataLoader.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import fs from 'fs';
22
import path from 'path';
33

4+
import type { CachedMetadata, FrontMatterCache } from 'obsidian';
5+
46
import type { SimulatedFile } from '../Obsidian/SimulatedFile';
57
import type { MockDataName } from '../Obsidian/AllCacheSampleData';
68

@@ -63,4 +65,79 @@ export class MockDataLoader {
6365
public static markdownPath(_testDataName: MockDataName) {
6466
return `Test Data/${_testDataName}.md`;
6567
}
68+
69+
/**
70+
* Find the {@link SimulatedFile} that contains the specified CachedMetadata.
71+
*
72+
* Searches through all cached {@link SimulatedFile} entries to find the one whose
73+
* cachedMetadata property is identical (by reference) to the provided value.
74+
*
75+
* @param cachedMetadata - The CachedMetadata object to search for
76+
* @returns The SimulatedFile containing the matching cachedMetadata
77+
* @throws Error if no matching SimulatedFile is found in the cache
78+
*/
79+
public static findCachedMetaData(cachedMetadata: CachedMetadata): SimulatedFile {
80+
return this.findByPredicate(
81+
(simulatedFile) => simulatedFile.cachedMetadata === cachedMetadata,
82+
'CachedMetadata not found in any loaded SimulatedFile',
83+
);
84+
}
85+
86+
/**
87+
* Find the {@link SimulatedFile} that contains the specified FrontMatterCache.
88+
*
89+
* Searches through all cached {@link SimulatedFile} entries to find the one whose
90+
* cachedMetadata.frontmatter property is identical (by reference) to the provided value.
91+
* This is useful for reverse-lookup when you have a FrontMatterCache object and need
92+
* to find which file it came from.
93+
*
94+
* @param frontmatter - The FrontMatterCache object to search for (can be undefined)
95+
* @returns The SimulatedFile containing the matching frontmatter
96+
* @throws Error if no matching SimulatedFile is found in the cache
97+
*/
98+
public static findFrontmatter(frontmatter: FrontMatterCache | undefined) {
99+
return this.findByPredicate(
100+
(simulatedFile) => simulatedFile.cachedMetadata.frontmatter === frontmatter,
101+
'FrontMatterCache not found in any loaded SimulatedFile. Did you supply TasksFile.frontmatter instead of TasksFile.cachedMetadata.frontmatter?',
102+
);
103+
}
104+
105+
/**
106+
* Find the {@link SimulatedFile} that matches the specified Markdown file path.
107+
*
108+
* Searches through all cached {@link SimulatedFile} entries to find the one whose
109+
* filePath property exactly matches the provided Markdown path. This enables
110+
* lookup of test data by the original file path from the test vault.
111+
*
112+
* @param markdownPath - The Markdown file path to search for (such as "Test Data/example.md")
113+
* @returns The SimulatedFile with the matching file path
114+
* @throws Error if no matching SimulatedFile is found in the cache
115+
*/
116+
public static findDataFromMarkdownPath(markdownPath: string) {
117+
return this.findByPredicate(
118+
(simulatedFile) => simulatedFile.filePath === markdownPath,
119+
'Markdown path not found in any loaded SimulatedFile',
120+
);
121+
}
122+
123+
/**
124+
* Helper method to find a SimulatedFile using a custom predicate function.
125+
*
126+
* @param predicate - Function that returns true when the desired SimulatedFile is found
127+
* @param errorMessage - Error message to throw if no matching SimulatedFile is found
128+
* @returns The first SimulatedFile that matches the predicate
129+
* @throws Error with the provided message if no match is found
130+
*/
131+
private static findByPredicate(
132+
predicate: (simulatedFile: SimulatedFile) => boolean,
133+
errorMessage: string,
134+
): SimulatedFile {
135+
for (const simulatedFile of this.cache.values()) {
136+
if (predicate(simulatedFile)) {
137+
return simulatedFile;
138+
}
139+
}
140+
141+
throw new Error(errorMessage);
142+
}
66143
}

tests/TestingTools/TaskBuilder.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { DateParser } from '../../src/DateTime/DateParser';
1010
import { StatusConfiguration, StatusType } from '../../src/Statuses/StatusConfiguration';
1111
import { TaskLocation } from '../../src/Task/TaskLocation';
1212
import { Priority } from '../../src/Task/Priority';
13-
import { setCurrentCacheFile } from '../__mocks__/obsidian';
1413
import type { ListItem } from '../../src/Task/ListItem';
1514
import type { SimulatedFile } from '../Obsidian/SimulatedFile';
1615
import type { MockDataName } from '../Obsidian/AllCacheSampleData';
@@ -79,9 +78,6 @@ export class TaskBuilder {
7978
if (this._tags.length > 0) {
8079
description += ' ' + this._tags.join(' ');
8180
}
82-
if (this._mockData !== undefined) {
83-
setCurrentCacheFile(this._mockData);
84-
}
8581
const cachedMetadata = this._mockData?.cachedMetadata ?? {};
8682
const task = new Task({
8783
// NEW_TASK_FIELD_EDIT_REQUIRED

tests/__mocks__/obsidian.ts

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { App, CachedMetadata, Reference } from 'obsidian';
22
import type { SimulatedFile } from '../Obsidian/SimulatedFile';
3+
import { MockDataLoader } from '../TestingTools/MockDataLoader';
34

45
export {};
56

@@ -132,44 +133,45 @@ function caseInsensitiveSubstringSearch(searchTerm: string, phrase: string): Sea
132133
: null;
133134
}
134135

135-
let mockedFileData: any = {};
136-
137-
export function setCurrentCacheFile(mockData: SimulatedFile) {
138-
mockedFileData = mockData;
139-
}
140-
141-
function reportInconsistentTestData(functionName: string) {
142-
throw new Error(
143-
`Inconsistent test data used in mock ${functionName}(). Check setCurrentCacheFile() has been called with the correct {@link SimulatedFile} data.`,
144-
);
145-
}
146-
147136
/**
148137
* Fake implementation of Obsidian's `getAllTags()`.
149138
*
150139
* See https://docs.obsidian.md/Reference/TypeScript+API/getAllTags
151140
*
152-
* @param cachedMetadata
141+
* @param cachedMetadata - the CachedMetadata instance from a SimulatedFile that has
142+
* already been loaded via MockDataLoader.get().
143+
* @throws Error if no matching CachedMetadata is found in the MockDataLoader cache.
153144
*/
154145
export function getAllTags(cachedMetadata: CachedMetadata): string[] {
155-
if (cachedMetadata !== mockedFileData.cachedMetadata) {
156-
reportInconsistentTestData('getAllTags');
157-
}
158-
return mockedFileData.getAllTags;
146+
const simulatedFile = MockDataLoader.findCachedMetaData(cachedMetadata);
147+
return simulatedFile.getAllTags;
159148
}
160149

161150
/**
162151
* Fake implementation of Obsidian's `parseFrontMatterTags()`.
163152
*
164153
* See https://docs.obsidian.md/Reference/TypeScript+API/parseFrontMatterTags
165154
*
166-
* @param frontmatter
155+
* @example
156+
* This works:
157+
* ```typescript
158+
* const tags = parseFrontMatterTags(tasksFile.cachedMetadata.frontmatter);
159+
* ```
160+
*
161+
* @example
162+
* This does not work:
163+
* ```typescript
164+
* const tags = parseFrontMatterTags(tasksFile.frontmatter);
165+
* ```
166+
*
167+
* @param frontmatter - the raw CachedMetadata.frontmatter instance from a SimulatedFile that has
168+
* already been loaded via MockDataLoader.get().
169+
* @throws Error if no matching frontmatter is found in the MockDataLoader cache,
170+
* or a `tasksFile.frontmatter` was supplied.
167171
*/
168172
export function parseFrontMatterTags(frontmatter: any | null): string[] | null {
169-
if (frontmatter !== mockedFileData.cachedMetadata.frontmatter) {
170-
reportInconsistentTestData('parseFrontMatterTags');
171-
}
172-
return mockedFileData.parseFrontMatterTags;
173+
const simulatedFile = MockDataLoader.findFrontmatter(frontmatter);
174+
return simulatedFile.parseFrontMatterTags;
173175
}
174176

175177
/**
@@ -180,7 +182,8 @@ export function parseFrontMatterTags(frontmatter: any | null): string[] | null {
180182
* See https://docs.obsidian.md/Reference/TypeScript+API/MetadataCache/getFirstLinkpathDest
181183
*
182184
* @param rawLink
183-
* @param sourcePath
185+
* @param sourcePath - the path to a Markdown file in the test vault whose SimulatedFile has already
186+
* been loaded via MockDataLoader.get(). For example, 'Test Data/callout.md'
184187
*
185188
* @example
186189
* ```typescript
@@ -192,13 +195,11 @@ export function parseFrontMatterTags(frontmatter: any | null): string[] | null {
192195
* ```
193196
*/
194197
export function getFirstLinkpathDest(rawLink: Reference, sourcePath: string): string | null {
195-
if (mockedFileData.filePath !== sourcePath) {
196-
reportInconsistentTestData('getFirstLinkpathDest');
197-
}
198-
return getFirstLinkpathDestFromData(mockedFileData, rawLink);
198+
const simulatedFile = MockDataLoader.findDataFromMarkdownPath(sourcePath);
199+
return getFirstLinkpathDestFromData(simulatedFile, rawLink);
199200
}
200201

201-
export function getFirstLinkpathDestFromData(data: any, rawLink: Reference) {
202+
export function getFirstLinkpathDestFromData(data: SimulatedFile, rawLink: Reference) {
202203
if (!(rawLink.link in data.resolveLinkToPath)) {
203204
console.log(`Cannot find resolved path for ${rawLink.link} in ${data.filePath} in mock getFirstLinkpathDest()`);
204205
}

0 commit comments

Comments
 (0)