mirror of
https://github.com/foambubble/foam.git
synced 2026-01-08 21:48:15 -05:00
Improved testing infrastructure (#1487)
* Added VS Code mock to turn e2e into unit/integration tests * Provide fallback to editor directory when creating new note with relative path * Added `clear` function to `FoamWorkspace` * Fixed tests for dated notes by providing configuration defaults * Using different workspace directory when resetting mock * tweaked test suite configuration to manage vscode mock * Tweaked test scripts to allow running specs in "unit" mode with mock vscode environment * Marked spec files that can be run in unit mode * Added testing documentation * removed --stream flag * updated @types/node to match engine's version * Fixing open-resource tests
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -73,4 +73,4 @@ jobs:
|
||||
- name: Run Tests
|
||||
uses: GabrielBB/xvfb-action@v1.4
|
||||
with:
|
||||
run: yarn test --stream
|
||||
run: yarn test
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -26,7 +26,7 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"jest.autoRun": "off",
|
||||
"jest.rootPath": "packages/foam-vscode",
|
||||
"jest.jestCommandLine": "yarn jest",
|
||||
"jest.jestCommandLine": "yarn test:unit-with-specs",
|
||||
"gitdoc.enabled": false,
|
||||
"search.mode": "reuseEditor",
|
||||
"[typescript]": {
|
||||
|
||||
130
docs/dev/testing.md
Normal file
130
docs/dev/testing.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Testing in Foam VS Code Extension
|
||||
|
||||
This document explains the testing strategy and conventions used in the Foam VS Code extension.
|
||||
|
||||
## Test File Types
|
||||
|
||||
We use two distinct types of test files, each serving different purposes:
|
||||
|
||||
### `.test.ts` Files - Pure Unit Tests
|
||||
|
||||
- **Purpose**: Test business logic and algorithms in complete isolation
|
||||
- **Dependencies**: No VS Code APIs dependencies
|
||||
- **Environment**: Pure Jest with Node.js
|
||||
- **Speed**: Very fast execution
|
||||
- **Location**: Throughout the codebase alongside source files
|
||||
|
||||
### `.spec.ts` Files - Integration Tests with VS Code APIs
|
||||
|
||||
- **Purpose**: Test features that integrate with VS Code APIs and user workflows
|
||||
- **Dependencies**: Will likely depend on VS Code APIs (`vscode` module), otherwise avoid incurring the performance hit
|
||||
- **Environment**: Can run in TWO environments:
|
||||
- **Mock Environment**: Jest with VS Code API mocks (fast)
|
||||
- **Real VS Code**: Full VS Code extension host (slow but comprehensive)
|
||||
- **Speed**: Depends on environment (see performance section below)
|
||||
- **Location**: Primarily in `src/features/` and service layers
|
||||
|
||||
## Key Principle: Environment Flexibility for `.spec.ts` Files
|
||||
|
||||
**`.spec.ts` files use VS Code APIs**, but they can run in different environments:
|
||||
|
||||
- **Mock Environment**: Uses our VS Code API mocks for speed
|
||||
- **Real VS Code**: Uses actual VS Code extension host for full integration testing
|
||||
|
||||
This dual-environment capability allows us to:
|
||||
|
||||
- Run specs quickly during development (mock environment)
|
||||
- Verify full integration during CI/CD (real VS Code environment)
|
||||
- Gradually migrate specs to mock-compatible implementations
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Test Type | Environment | Typical Duration | VS Code APIs |
|
||||
| --------------------- | ---------------------- | ---------------- | ---------------- |
|
||||
| **`.test.ts`** | Pure Jest | fastest | **No** |
|
||||
| **`.spec.ts` (mock)** | Jest + VS Code Mocks | fast | **Yes** (mocked) |
|
||||
| **`.spec.ts` (real)** | VS Code Extension Host | sloooooow. | **Yes** (real) |
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Available Commands
|
||||
|
||||
- **`yarn test:unit`**: Runs only `.test.ts` files (no VS Code dependencies)
|
||||
- **`yarn test:unit-with-specs`**: Runs `.test.ts` + `@unit-ready` marked `.spec.ts` files using mocks
|
||||
- **`yarn test:e2e`**: Runs all `.spec.ts` files in full VS Code extension host
|
||||
- **`yarn test`**: Runs both unit and e2e test suites sequentially
|
||||
|
||||
## Mock Environment Migration
|
||||
|
||||
We're gradually enabling `.spec.ts` files to run in our fast mock environment while maintaining their ability to run in real VS Code.
|
||||
|
||||
### The `@unit-ready` Annotation
|
||||
|
||||
Spec files marked with `/* @unit-ready */` can run in both environments:
|
||||
|
||||
```typescript
|
||||
/* @unit-ready */
|
||||
import * as vscode from 'vscode';
|
||||
// ... test uses VS Code APIs but works with our mocks
|
||||
```
|
||||
|
||||
### Common Migration Fixes
|
||||
|
||||
**Configuration defaults**: Our mocks don't load package.json defaults
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const format = getFoamVsCodeConfig('openDailyNote.filenameFormat');
|
||||
|
||||
// After (defensive)
|
||||
const format = getFoamVsCodeConfig(
|
||||
'openDailyNote.filenameFormat',
|
||||
'yyyy-mm-dd'
|
||||
);
|
||||
```
|
||||
|
||||
**File system operations**: Ensure proper async handling
|
||||
|
||||
```typescript
|
||||
// Mock file operations are immediate but still async
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
```
|
||||
|
||||
### When NOT to Migrate
|
||||
|
||||
Some specs should remain real-VS-Code-only:
|
||||
|
||||
- Tests verifying complex VS Code UI interactions
|
||||
- Tests requiring real file system watching with timing
|
||||
- Tests validating extension packaging or activation
|
||||
- Tests that depend on VS Code's complex internal state management
|
||||
|
||||
## Mock System Capabilities
|
||||
|
||||
Our `vscode-mock.ts` provides comprehensive VS Code API mocking:
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
When adding new tests:
|
||||
|
||||
1. **Choose the right type**:
|
||||
|
||||
- Use `.test.ts` for pure business logic with no VS Code dependencies
|
||||
- Use `.spec.ts` for anything that needs VS Code APIs
|
||||
|
||||
2. **Consider mock compatibility**:
|
||||
|
||||
- When writing `.spec.ts` files, consider if they could run in mock environment
|
||||
- Add `/* @unit-ready */` if the test works with our mocks
|
||||
|
||||
3. **Follow naming conventions**:
|
||||
|
||||
- Test files should be co-located with source files when possible
|
||||
- Use descriptive test names that explain the expected behavior
|
||||
|
||||
4. **Performance awareness**:
|
||||
- Prefer unit tests for business logic (fastest)
|
||||
- Use mock-compatible specs for VS Code integration (fast)
|
||||
- Reserve real VS Code specs for complex integration scenarios (comprehensive)
|
||||
|
||||
This testing strategy gives us the best of both worlds: fast feedback during development and comprehensive integration verification when needed.
|
||||
@@ -16,9 +16,9 @@
|
||||
"reset": "yarn && yarn clean && yarn build",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "yarn workspace foam-vscode test --stream",
|
||||
"test": "yarn workspace foam-vscode test",
|
||||
"lint": "lerna run lint",
|
||||
"watch": "lerna run watch --concurrency 20 --stream"
|
||||
"watch": "lerna run watch --concurrency 20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"all-contributors-cli": "^6.16.1",
|
||||
|
||||
@@ -123,7 +123,7 @@ module.exports = {
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
@@ -153,9 +153,8 @@ module.exports = {
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// This is overridden in every runCLI invocation but it's here as the default
|
||||
// for vscode-jest. We only want unit tests in the test explorer (sidebar),
|
||||
// since spec tests require the entire extension host to be launched before.
|
||||
testRegex: ['\\.test\\.ts$'],
|
||||
// for vscode-jest. Both .test.ts and .spec.ts files use the vscode-mock.
|
||||
testRegex: ['\\.(test|spec)\\.ts$'],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
@@ -678,7 +678,8 @@
|
||||
"test-reset-workspace": "rm -rf .test-workspace && mkdir .test-workspace && touch .test-workspace/.keep",
|
||||
"test-setup": "yarn compile && yarn build && yarn test-reset-workspace",
|
||||
"test": "yarn test-setup && node ./out/test/run-tests.js",
|
||||
"test:unit": "yarn test-setup && node ./out/test/run-tests.js --unit",
|
||||
"test:unit": "yarn test-setup && node ./out/test/run-tests.js --unit --exclude-specs",
|
||||
"test:unit-with-specs": "yarn test-setup && node ./out/test/run-tests.js --unit",
|
||||
"test:e2e": "yarn test-setup && node ./out/test/run-tests.js --e2e",
|
||||
"lint": "dts lint src",
|
||||
"clean": "rimraf out",
|
||||
@@ -697,7 +698,7 @@
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
"@types/node": "^13.11.0",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/picomatch": "^2.2.1",
|
||||
"@types/remove-markdown": "^0.1.1",
|
||||
"@types/vscode": "^1.70.0",
|
||||
|
||||
@@ -52,6 +52,16 @@ export class FoamWorkspace implements IDisposable {
|
||||
return deleted ?? null;
|
||||
}
|
||||
|
||||
clear() {
|
||||
const resources = Array.from(this._resources.values());
|
||||
this._resources.clear();
|
||||
|
||||
// Fire delete events for all resources
|
||||
resources.forEach(resource => {
|
||||
this.onDidDeleteEmitter.fire(resource);
|
||||
});
|
||||
}
|
||||
|
||||
public exists(uri: URI): boolean {
|
||||
return isSome(this.find(uri));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { workspace } from 'vscode';
|
||||
import { createDailyNoteIfNotExists, getDailyNoteUri } from './dated-notes';
|
||||
import { isWindows } from './core/common/platform';
|
||||
|
||||
@@ -54,10 +54,12 @@ export function getDailyNoteUri(date: Date): URI {
|
||||
*/
|
||||
export function getDailyNoteFileName(date: Date): string {
|
||||
const filenameFormat: string = getFoamVsCodeConfig(
|
||||
'openDailyNote.filenameFormat'
|
||||
'openDailyNote.filenameFormat',
|
||||
'yyyy-mm-dd'
|
||||
);
|
||||
const fileExtension: string = getFoamVsCodeConfig(
|
||||
'openDailyNote.fileExtension'
|
||||
'openDailyNote.fileExtension',
|
||||
'md'
|
||||
);
|
||||
|
||||
return `${dateFormat(date, filenameFormat, false)}.${fileExtension}`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { env, Position, Selection, commands } from 'vscode';
|
||||
import { createFile, showInEditor } from '../../test/test-utils-vscode';
|
||||
import { removeBrackets, toTitleCase } from './copy-without-brackets';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { createFile } from '../../test/test-utils-vscode';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { asAbsoluteWorkspaceUri, readFile } from '../../services/editor';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import dateFormat from 'dateformat';
|
||||
import { commands, window } from 'vscode';
|
||||
|
||||
|
||||
@@ -3,19 +3,28 @@ import { CommandDescriptor } from '../../utils/commands';
|
||||
import { OpenResourceArgs, OPEN_COMMAND } from './open-resource';
|
||||
import * as filter from '../../core/services/resource-filter';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { closeEditors, createFile } from '../../test/test-utils-vscode';
|
||||
import {
|
||||
closeEditors,
|
||||
createFile,
|
||||
waitForNoteInFoamWorkspace,
|
||||
} from '../../test/test-utils-vscode';
|
||||
import { deleteFile } from '../../services/editor';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
|
||||
describe('open-resource command', () => {
|
||||
beforeEach(async () => {
|
||||
await jest.resetAllMocks();
|
||||
jest.resetAllMocks();
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
it('URI param has precedence over filter', async () => {
|
||||
const spy = jest.spyOn(filter, 'createFilter');
|
||||
const noteA = await createFile('Note A for open command');
|
||||
await waitForNoteInFoamWorkspace(noteA.uri);
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
@@ -26,7 +35,8 @@ describe('open-resource command', () => {
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(window.activeTextEditor).toBeTruthy();
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
@@ -36,15 +46,17 @@ describe('open-resource command', () => {
|
||||
|
||||
it('URI param accept URI object, or path', async () => {
|
||||
const noteA = await createFile('Note A for open command');
|
||||
await waitForNoteInFoamWorkspace(noteA.uri);
|
||||
|
||||
const uriCommand: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/file.md'),
|
||||
uri: noteA.uri,
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(uriCommand.name, uriCommand.params);
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(window.activeTextEditor).toBeTruthy();
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
|
||||
@@ -53,17 +65,18 @@ describe('open-resource command', () => {
|
||||
const pathCommand: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/file.md'),
|
||||
uri: noteA.uri.path,
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(pathCommand.name, pathCommand.params);
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(window.activeTextEditor).toBeTruthy();
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
await deleteFile(noteA.uri);
|
||||
});
|
||||
|
||||
it('User is notified if no resource is found', async () => {
|
||||
it('User is notified if no resource is found with filter', async () => {
|
||||
const spy = jest.spyOn(window, 'showInformationMessage');
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
@@ -74,12 +87,33 @@ describe('open-resource command', () => {
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('User is notified if no resource is found with URI', async () => {
|
||||
const spy = jest.spyOn(window, 'showInformationMessage');
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/nonexistent.md'),
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
await waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('filter with multiple results will show a quick pick', async () => {
|
||||
const noteA = await createFile('Note A for filter test');
|
||||
const noteB = await createFile('Note B for filter test');
|
||||
await waitForNoteInFoamWorkspace(noteA.uri);
|
||||
await waitForNoteInFoamWorkspace(noteB.uri);
|
||||
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
@@ -92,8 +126,11 @@ describe('open-resource command', () => {
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await deleteFile(noteA.uri);
|
||||
await deleteFile(noteB.uri);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,13 +82,18 @@ async function openResource(workspace: FoamWorkspace, args?: OpenResourceArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
if (isSome(item)) {
|
||||
const targetUri =
|
||||
item.uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(item.uri.asPlain());
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri);
|
||||
if (isNone(item)) {
|
||||
vscode.window.showInformationMessage(
|
||||
'Foam: No note matches given filters or URI.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUri =
|
||||
item.uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(item.uri.asPlain());
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri);
|
||||
}
|
||||
|
||||
interface ResourceItem extends vscode.QuickPickItem {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { workspace, window } from 'vscode';
|
||||
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { createTestNote } from '../../test/test-utils';
|
||||
import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
|
||||
import { TagItem, TagsProvider } from './tags-explorer';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { default as markdownItFoamTags } from './tag-highlight';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { createMarkdownParser } from '../../core/services/markdown-parser';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { createTestNote } from '../../test/test-utils';
|
||||
|
||||
@@ -188,7 +188,7 @@ export async function readFile(uri: URI): Promise<string | undefined> {
|
||||
if (await fileExists(uri)) {
|
||||
return workspace.fs
|
||||
.readFile(toVsCodeUri(uri))
|
||||
.then(bytes => bytes.toString());
|
||||
.then(bytes => new TextDecoder('utf-8').decode(bytes));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -223,7 +223,11 @@ const createFnForOnRelativePathStrategy =
|
||||
|
||||
switch (onRelativePath) {
|
||||
case 'resolve-from-current-dir':
|
||||
return getCurrentEditorDirectory().joinPath(existingFile.path);
|
||||
try {
|
||||
return getCurrentEditorDirectory().joinPath(existingFile.path);
|
||||
} catch (e) {
|
||||
return asAbsoluteWorkspaceUri(existingFile);
|
||||
}
|
||||
case 'resolve-from-root':
|
||||
return asAbsoluteWorkspaceUri(existingFile);
|
||||
case 'cancel':
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { getNotesExtensions } from './settings';
|
||||
import { withModifiedFoamConfiguration } from './test/test-utils-vscode';
|
||||
|
||||
|
||||
@@ -2,28 +2,36 @@ import path from 'path';
|
||||
import { runTests } from 'vscode-test';
|
||||
import { runUnit } from './suite-unit';
|
||||
|
||||
function parseArgs(): { unit: boolean; e2e: boolean; jestArgs: string[] } {
|
||||
function parseArgs(): {
|
||||
unit: boolean;
|
||||
e2e: boolean;
|
||||
excludeSpecs: boolean;
|
||||
jestArgs: string[];
|
||||
} {
|
||||
const args = process.argv.slice(2);
|
||||
const unit = args.some(arg => arg === '--unit');
|
||||
const e2e = args.some(arg => arg === '--e2e');
|
||||
const unit = args.includes('--unit');
|
||||
const e2e = args.includes('--e2e');
|
||||
const excludeSpecs = args.includes('--exclude-specs');
|
||||
|
||||
// Filter out our custom flags and pass the rest to Jest
|
||||
const jestArgs = args.filter(arg => arg !== '--unit' && arg !== '--e2e');
|
||||
const jestArgs = args.filter(
|
||||
arg => !['--unit', '--e2e', '--exclude-specs'].includes(arg)
|
||||
);
|
||||
|
||||
return unit || e2e
|
||||
? { unit, e2e, jestArgs }
|
||||
: { unit: true, e2e: true, jestArgs };
|
||||
? { unit, e2e, excludeSpecs, jestArgs }
|
||||
: { unit: true, e2e: true, excludeSpecs, jestArgs };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { unit, e2e, jestArgs } = parseArgs();
|
||||
const { unit, e2e, excludeSpecs, jestArgs } = parseArgs();
|
||||
|
||||
let isSuccess = true;
|
||||
|
||||
if (unit) {
|
||||
try {
|
||||
console.log('Running unit tests');
|
||||
await runUnit(jestArgs);
|
||||
await runUnit(jestArgs, excludeSpecs);
|
||||
} catch (err) {
|
||||
console.log('Error occurred while running Foam unit tests:', err);
|
||||
isSuccess = false;
|
||||
|
||||
@@ -18,10 +18,43 @@ process.env.NODE_ENV = 'test';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { runCLI } from '@jest/core';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as glob from 'glob';
|
||||
|
||||
const rootDir = path.join(__dirname, '..', '..');
|
||||
|
||||
export function runUnit(extraArgs: string[] = []): Promise<void> {
|
||||
function getUnitReadySpecFiles(rootDir: string): string[] {
|
||||
const specFiles = glob.sync('**/*.spec.ts', {
|
||||
cwd: path.join(rootDir, 'src'),
|
||||
});
|
||||
const unitReadyFiles: string[] = [];
|
||||
|
||||
for (const file of specFiles) {
|
||||
const fullPath = path.join(rootDir, 'src', file);
|
||||
try {
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
// Check for @unit-ready annotation in file
|
||||
if (
|
||||
content.includes('/* @unit-ready */') ||
|
||||
content.includes('// @unit-ready')
|
||||
) {
|
||||
unitReadyFiles.push(file);
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip files that can't be read
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return unitReadyFiles;
|
||||
}
|
||||
|
||||
export function runUnit(
|
||||
extraArgs: string[] = [],
|
||||
excludeSpecs = false
|
||||
): Promise<void> {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
@@ -30,8 +63,24 @@ export function runUnit(extraArgs: string[] = []): Promise<void> {
|
||||
rootDir,
|
||||
roots: ['<rootDir>/src'],
|
||||
runInBand: true,
|
||||
testRegex: '\\.(test)\\.ts$',
|
||||
testRegex: excludeSpecs
|
||||
? ['\\.(test)\\.ts$']
|
||||
: (() => {
|
||||
const unitReadySpecs = getUnitReadySpecFiles(rootDir);
|
||||
|
||||
// Create pattern that includes .test files + specific .spec files
|
||||
return [
|
||||
'\\.(test)\\.ts$', // All .test files
|
||||
...unitReadySpecs.map(
|
||||
file =>
|
||||
file.replace(/\//g, '\\/').replace(/\./g, '\\.') + '$'
|
||||
),
|
||||
];
|
||||
})(),
|
||||
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/test/support/jest-setup-after-env.ts',
|
||||
],
|
||||
testTimeout: 20000,
|
||||
verbose: false,
|
||||
silent: false,
|
||||
|
||||
@@ -58,7 +58,7 @@ export function run(): Promise<void> {
|
||||
runInBand: true,
|
||||
testRegex: '\\.(test|spec)\\.ts$',
|
||||
testEnvironment: '<rootDir>/src/test/support/vscode-environment.js',
|
||||
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
|
||||
setupFiles: ['<rootDir>/src/test/support/jest-setup-e2e.ts'],
|
||||
testTimeout: 30000,
|
||||
useStderr: true,
|
||||
verbose: true,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file runs in the test environment where Jest globals are available
|
||||
|
||||
// Clean up after each test file to prevent hanging threads
|
||||
afterAll(async () => {
|
||||
const vscode = require('../vscode-mock');
|
||||
|
||||
// Force cleanup of any async operations
|
||||
if (vscode.forceCleanup) {
|
||||
await vscode.forceCleanup();
|
||||
}
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
// Wait for any remaining async operations to complete
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
});
|
||||
2
packages/foam-vscode/src/test/support/jest-setup-e2e.ts
Normal file
2
packages/foam-vscode/src/test/support/jest-setup-e2e.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/config/jestSetup.ts
|
||||
jest.mock('vscode', () => (global as any).vscode, { virtual: true });
|
||||
@@ -1,2 +1 @@
|
||||
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/config/jestSetup.ts
|
||||
jest.mock('vscode', () => (global as any).vscode, { virtual: true });
|
||||
jest.mock('vscode', () => require('../vscode-mock'), { virtual: true });
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Logger } from '../core/utils/log';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { randomString, wait } from './test-utils';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Foam } from '../core/model/foam';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -53,6 +55,35 @@ export const getUriInWorkspace = (...filepath: string[]) => {
|
||||
return uri;
|
||||
};
|
||||
|
||||
export const getFoamFromVSCode = async (): Promise<Foam> => {
|
||||
// In test environment, try different extension IDs
|
||||
const extension = vscode.extensions.getExtension('foam.foam-vscode');
|
||||
|
||||
const exports = extension.isActive
|
||||
? extension.exports
|
||||
: await extension.activate();
|
||||
if (!exports || !exports.foam) {
|
||||
throw new Error('Foam not available in extension exports');
|
||||
}
|
||||
|
||||
return exports.foam;
|
||||
};
|
||||
|
||||
export const waitForNoteInFoamWorkspace = async (uri: URI, timeout = 5000) => {
|
||||
const start = Date.now();
|
||||
const foam = await getFoamFromVSCode();
|
||||
const workspace = foam.workspace;
|
||||
|
||||
// Wait for the workspace to discover the note
|
||||
while (Date.now() - start < timeout) {
|
||||
if (workspace.find(uri.path)) {
|
||||
return true;
|
||||
}
|
||||
await wait(100);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a file with a some content.
|
||||
*
|
||||
|
||||
1639
packages/foam-vscode/src/test/vscode-mock.ts
Normal file
1639
packages/foam-vscode/src/test/vscode-mock.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user