Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add and Update Integration Tests #1168

Merged
merged 9 commits into from
Sep 13, 2024
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
'parserOptions': {
'ecmaVersion': 12,
'sourceType': 'module',
'project': ['./tsconfig.json', './packages/*/tsconfig.json'],
},
'plugins': [
'@typescript-eslint',
Expand All @@ -29,6 +30,7 @@ module.exports = {
'require-jsdoc': 'off',
'unicorn/template-indent': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
Expand Down
30 changes: 0 additions & 30 deletions __integration__/custom-commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import dedent from 'ts-dedent';
import TestLinterPlugin, {IntegrationTestCase} from './main.test';
import {Editor} from 'obsidian';
import expect from 'expect';

export const customCommandTestCases: IntegrationTestCase[] = [
{
Expand All @@ -17,33 +15,5 @@ export const customCommandTestCases: IntegrationTestCase[] = [
'name': 'Format all tables in this file',
}];
},
async assertions(editor: Editor) {
expect(editor.getValue()).toBe(dedent`
| one | two | three | four |
| ----- | --- | ----- | ------------- |
| 1 | ✓ | | |
| 2 | | ✓ | |
| 3 | | ✓ | ✓ |
| 4 | … | | someting else |
| Total | 100 | 20 | 300000000 |
${''}
| one | two | three | four |
| ----- | --- | ----- | ------------- |
| 1 | ✓ | | |
| 2 | | ✓ | |
| 3 | | ✓ | ✓ |
| 4 | … | | someting else |
| Total | 100 | 20 | 300000000 |
${''}
| one | two | three | four |
| ----- | --- | ----- | ------------- |
| 1 | ✓ | | |
| 2 | | ✓ | |
| 3 | | ✓ | ✓ |
| 4 | … | | someting else |
| Total | 100 | 20 | 300000000 |
${''}
`);
},
},
];
193 changes: 173 additions & 20 deletions __integration__/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,61 @@
import {Editor, MarkdownView, Plugin, TFile, normalizePath} from 'obsidian';
import {Editor, MarkdownView, Notice, Plugin, TFile, normalizePath} from 'obsidian';
import LinterPlugin from 'src/main';
import {obsidianModeTestCases} from './obsidian-mode.test';
import {setWorkspaceItemMode} from './utils.test';
import {customCommandTestCases} from './custom-commands.test';
import {obsidianYAMLRuleTestCases} from './yaml-rule.test';
import expect from 'expect';

export type IntegrationTestCase = {
name: string,
filePath: string,
setup?: (plugin: TestLinterPlugin, editor: Editor) => void,
assertions: (editor: Editor) => void,
setup?: (plugin: TestLinterPlugin, editor: Editor) => Promise<void>,
assertions?: (editor: Editor) => void,
modifyExpected?: (expectedText: string, file: TFile) => string,
}

type testStatus = {
name: string,
succeeded: boolean,
}

const testTimeout = 15000;

export default class TestLinterPlugin extends Plugin {
regularTests: Array<IntegrationTestCase> = [...obsidianModeTestCases];
regularTests: Array<IntegrationTestCase> = [...obsidianModeTestCases, ...obsidianYAMLRuleTestCases];
afterCacheUpdateTests: Array<IntegrationTestCase> = [...customCommandTestCases];
plugin: LinterPlugin;
private timeoutId: any = undefined;
private testRunNotice: Notice;

async onload() {
this.addCommand({
id: 'run-linter-tests',
name: 'Run Linter Tests',
callback: async () => {
if (this.timeoutId != undefined) {
clearTimeout(this.timeoutId);
}

await this.setup();
await this.runTests();

const testStatuses = [] as testStatus[];
const expectedTestCount = this.regularTests.length + this.afterCacheUpdateTests.length;
this.timeoutId = setTimeout(() => {
console.log(testStatuses);
if (testStatuses.length != expectedTestCount) {
if (this.testRunNotice) {
this.testRunNotice.setMessage(`❌: Tests took too long to run with only ${testStatuses.length} of ${expectedTestCount} tests running in ${testTimeout/1000}s.`);
} else {
console.log('❌', `Tests took too long to run with only ${testStatuses.length} of ${expectedTestCount} tests running in ${testTimeout/1000}s.`);
}
} else {
this.handleTestFinalization(testStatuses);
console.log(`✅ all ${expectedTestCount} tests have completed in the alloted time.`);
}
}, testTimeout);

await this.runTests(testStatuses, expectedTestCount);
},
});
}
Expand All @@ -37,7 +70,9 @@ export default class TestLinterPlugin extends Plugin {
}
}

async runTests() {
async runTests(testStatuses: testStatus[], totalTestCount: number) {
this.testRunNotice = new Notice('Starting the Linter\'s Integration Tests', 0);

const activeLeaf = this.getActiveLeaf();
if (!activeLeaf) {
console.error('failed to get active leaf');
Expand All @@ -48,6 +83,8 @@ export default class TestLinterPlugin extends Plugin {
const file = this.getFileFromPath(t.filePath);
if (!file) {
console.error('failed to get file: ' + t.filePath);

this.handleTestCompletion(t.name, false, testStatuses, totalTestCount);
continue;
}

Expand All @@ -61,23 +98,35 @@ export default class TestLinterPlugin extends Plugin {
}

await this.plugin.runLinterEditor(activeLeaf.editor);
await t.assertions(activeLeaf.editor);
await this.handleAssertions(t, activeLeaf, file);

console.log('✅', t.name);
this.handleTestCompletion(t.name, true, testStatuses, totalTestCount);
} catch (e) {
console.log('❌', t.name);
console.error(e);

this.handleTestCompletion(t.name, false, testStatuses, totalTestCount);
}

await this.resetFileContents(activeLeaf, originalText);
}

await this.runMetadataTests(this.afterCacheUpdateTests, activeLeaf);
if (testStatuses.length != this.regularTests.length) {
if (this.testRunNotice) {
this.testRunNotice.setMessage(`❌ failed to run all ${this.regularTests.length} regular tests before attempting to start the metadata tests.`);
} else {
console.log(`❌ failed to run all ${this.regularTests.length} regular tests before attempting to start the metadata tests.`);
}
return;
}

await this.runMetadataTests(this.afterCacheUpdateTests, activeLeaf, testStatuses, totalTestCount);
}

async runMetadataTests(tests: IntegrationTestCase[], activeLeaf: MarkdownView) {
async runMetadataTests(tests: IntegrationTestCase[], activeLeaf: MarkdownView, testStatuses: testStatus[], totalTestCount: number) {
let index = 0;
let originalText = await this.setupMetadataTest(this, tests[index], activeLeaf);
let originalText = await this.setupMetadataTest(this, tests[index], activeLeaf, testStatuses, totalTestCount);
if (originalText == null) {
return;
}
Expand All @@ -95,29 +144,29 @@ export default class TestLinterPlugin extends Plugin {

const t = tests[index];
try {
await t.assertions(activeLeaf.editor);
await this.handleAssertions(t, activeLeaf, file);

console.log('✅', t.name);
this.handleTestCompletion(t.name, true, testStatuses, totalTestCount);
} catch (e) {
console.log('❌', t.name);
console.error(e);

this.handleTestCompletion(t.name, false, testStatuses, totalTestCount);
}

await that.resetFileContents(activeLeaf, originalText);

originalText = null;
while (index+1 < tests.length && originalText == null) {
originalText = await that.setupMetadataTest(that, tests[++index], activeLeaf);
}

// remove the custom commands callback once all tests have run
if (index >= tests.length && originalText == null) {
if (index+1 < tests.length) {
originalText = await that.setupMetadataTest(that, tests[++index], activeLeaf, testStatuses, totalTestCount);
} else { // remove the custom commands callback once all tests have run
that.plugin.setCustomCommandCallback(null);
}
});
}

async setupMetadataTest(testPlugin: TestLinterPlugin, t: IntegrationTestCase, activeLeaf: MarkdownView): Promise<string> {
async setupMetadataTest(testPlugin: TestLinterPlugin, t: IntegrationTestCase, activeLeaf: MarkdownView, testStatuses: testStatus[], totalTestCount: number): Promise<string> {
const file = this.getFileFromPath(t.filePath);
if (!file) {
console.error('failed to get file: ' + t.filePath);
Expand All @@ -135,6 +184,8 @@ export default class TestLinterPlugin extends Plugin {

await testPlugin.plugin.runLinterEditor(activeLeaf.editor);
} catch (e) {
this.handleTestCompletion(t.name, false, testStatuses, totalTestCount);

console.log('❌', t.name);
console.error(e);
await testPlugin.resetFileContents(activeLeaf, originalText);
Expand All @@ -145,10 +196,26 @@ export default class TestLinterPlugin extends Plugin {
return originalText;
}

onunload(): void {
async onunload(): Promise<void> {
if (this.plugin) {
this.plugin.onunload();
await this.plugin.onunload();
}
}

private async handleAssertions(t: IntegrationTestCase, activeLeaf: MarkdownView, file: TFile) {
let expectedText = await this.getExpectedContents(t.filePath.replace('.md', '.linted.md'));
if (t.modifyExpected) {
expectedText = t.modifyExpected(expectedText, file);
}

expect(activeLeaf.editor.getValue()).toBe(expectedText);
if (t.assertions) {
t.assertions(activeLeaf.editor);
}

console.log('assertions complete for ' + t.filePath);

return;
}

private async resetFileContents(activeLeaf: MarkdownView, originalText: string) {
Expand All @@ -164,6 +231,16 @@ export default class TestLinterPlugin extends Plugin {
return activeLeaf;
}

private async getExpectedContents(filePath: string): Promise<string> {
const file = this.getFileFromPath(filePath);
if (!file) {
console.error('failed to get file: ' + filePath);
return;
}

return await this.app.vault.cachedRead(file);
}

private getFileFromPath(filePath: string): TFile {
const file = this.app.vault.getAbstractFileByPath(normalizePath(filePath));
if (file instanceof TFile) {
Expand All @@ -176,4 +253,80 @@ export default class TestLinterPlugin extends Plugin {
private async resetSettings() {
await this.plugin.loadSettings();
}

private handleTestCompletion(testName: string, succeeded: boolean, testStatuses: testStatus[], totalTestCount: number) {
testStatuses.push(
{
name: testName,
succeeded: succeeded,
});

let numberOfSuccesses = 0;
let numberOfFailures = 0;
for (const testResult of testStatuses) {
if (testResult.succeeded) {
numberOfSuccesses++;
} else {
numberOfFailures++;
}
}

if (this.testRunNotice) {
let message = `Running the Linter's Integration Tests (${testStatuses.length}/${totalTestCount})`;
message += '\nSo far there ';

if (numberOfFailures == 1) {
message += 'has been 1 failure';
} else {
message += `have been ${numberOfFailures} failures`;
}

message += ' and there ';

if (numberOfSuccesses == 1) {
message += 'has been 1 success';
} else {
message += `have been ${numberOfSuccesses} successes`;
}

message += '.';

this.testRunNotice.setMessage(message);
}
}

private handleTestFinalization(testStatuses: testStatus[]) {
if (this.testRunNotice) {
let message = `Finished running the Linter's Integration Tests.`;
message += '\nThere ';

let numberOfSuccesses = 0;
let numberOfFailures = 0;
for (const testResult of testStatuses) {
if (testResult.succeeded) {
numberOfSuccesses++;
} else {
numberOfFailures++;
}
}

if (numberOfFailures == 1) {
message += 'was 1 failure';
} else {
message += `have been ${numberOfFailures} failures`;
}

message += ' and there ';

if (numberOfSuccesses == 1) {
message += 'has been 1 success';
} else {
message += `have been ${numberOfSuccesses} successes`;
}

message += '. See the console for more details.';

this.testRunNotice.setMessage(message);
}
}
}
Loading
Loading