test: migrate jest to vitest

This commit is contained in:
Louis 2025-07-10 21:12:59 +07:00
parent 1c7a20be44
commit a770e08013
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
50 changed files with 202 additions and 299 deletions

View File

@ -68,7 +68,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: ref-lcov.info
path: coverage/merged/lcov.info
path: coverage/lcov.info
test-on-macos:
runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'macos-latest' || 'macos-selfhosted-12-arm64' }}
@ -263,7 +263,7 @@ jobs:
uses: barecheck/code-coverage-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
lcov-file: './coverage/merged/lcov.info'
lcov-file: './coverage/lcov.info'
base-lcov-file: './lcov.info'
send-summary-comment: true
show-annotations: 'warning'

View File

@ -108,7 +108,6 @@ jobs:
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
if [ "${{ inputs.channel }}" != "stable" ]; then
jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun",
"usr/lib/Jan-${{ inputs.channel }}/binaries": "binaries/deps",
"usr/lib/Jan-${{ inputs.channel }}/resources/lib/libvulkan.so": "resources/lib/libvulkan.so"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json
mv /tmp/tauri.linux.conf.json ./src-tauri/tauri.linux.conf.json
fi

View File

@ -1,17 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
runner: './testRunner.js',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
diagnostics: false,
},
],
},
}

View File

@ -17,27 +17,28 @@
"author": "Jan <service@jan.ai>",
"scripts": {
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
"test": "jest",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"prebuild": "rimraf dist",
"build": "tsc -p . && rolldown -c rolldown.config.mjs"
},
"devDependencies": {
"@npmcli/arborist": "^7.1.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.0",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"eslint": "8.57.0",
"eslint-plugin-jest": "^27.9.0",
"jest": "^30.0.3",
"jest-junit": "^16.0.0",
"jest-runner": "^30.0.3",
"happy-dom": "^15.11.6",
"pacote": "^21.0.0",
"request": "^2.88.2",
"request-progress": "^3.0.0",
"rimraf": "^6.0.1",
"rolldown": "1.0.0-beta.1",
"ts-jest": "^29.2.5",
"tslib": "^2.6.2",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^2.1.8"
},
"dependencies": {
"rxjs": "^7.8.1",

View File

@ -1,6 +1,4 @@
/**
* @jest-environment jsdom
*/
import { describe, it, expect, vi } from 'vitest'
import { openExternalUrl } from './core'
import { joinPath } from './core'
import { openFileExplorer } from './core'
@ -12,7 +10,7 @@ describe('test core apis', () => {
const url = 'http://example.com'
globalThis.core = {
api: {
openExternalUrl: jest.fn().mockResolvedValue('opened'),
openExternalUrl: vi.fn().mockResolvedValue('opened'),
},
}
const result = await openExternalUrl(url)
@ -24,7 +22,7 @@ describe('test core apis', () => {
const paths = ['/path/one', '/path/two']
globalThis.core = {
api: {
joinPath: jest.fn().mockResolvedValue('/path/one/path/two'),
joinPath: vi.fn().mockResolvedValue('/path/one/path/two'),
},
}
const result = await joinPath(paths)
@ -36,7 +34,7 @@ describe('test core apis', () => {
const path = '/path/to/open'
globalThis.core = {
api: {
openFileExplorer: jest.fn().mockResolvedValue('opened'),
openFileExplorer: vi.fn().mockResolvedValue('opened'),
},
}
const result = await openFileExplorer(path)
@ -47,7 +45,7 @@ describe('test core apis', () => {
it('should get jan data folder path', async () => {
globalThis.core = {
api: {
getJanDataFolderPath: jest.fn().mockResolvedValue('/path/to/jan/data'),
getJanDataFolderPath: vi.fn().mockResolvedValue('/path/to/jan/data'),
},
}
const result = await getJanDataFolderPath()
@ -58,7 +56,7 @@ describe('test core apis', () => {
describe('dirName - just a pass thru api', () => {
it('should retrieve the directory name from a file path', async () => {
const mockDirName = jest.fn()
const mockDirName = vi.fn()
globalThis.core = {
api: {
dirName: mockDirName.mockResolvedValue('/path/to'),

View File

@ -1,11 +1,11 @@
import { it, expect, vi } from 'vitest'
import { events } from './events';
import { jest } from '@jest/globals';
it('should emit an event', () => {
const mockObject = { key: 'value' };
globalThis.core = {
events: {
emit: jest.fn()
emit: vi.fn()
}
};
events.emit('testEvent', mockObject);
@ -14,10 +14,10 @@ it('should emit an event', () => {
it('should remove an observer for an event', () => {
const mockHandler = jest.fn();
const mockHandler = vi.fn();
globalThis.core = {
events: {
off: jest.fn()
off: vi.fn()
}
};
events.off('testEvent', mockHandler);
@ -26,10 +26,10 @@ it('should remove an observer for an event', () => {
it('should add an observer for an event', () => {
const mockHandler = jest.fn();
const mockHandler = vi.fn();
globalThis.core = {
events: {
on: jest.fn()
on: vi.fn()
}
};
events.on('testEvent', mockHandler);

View File

@ -1,7 +1,8 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { BaseExtension } from './extension'
import { SettingComponentProps } from '../types'
jest.mock('./core')
jest.mock('./fs')
vi.mock('./core')
vi.mock('./fs')
class TestBaseExtension extends BaseExtension {
onLoad(): void {}
@ -16,7 +17,7 @@ describe('BaseExtension', () => {
})
afterEach(() => {
jest.resetAllMocks()
vi.clearAllMocks()
})
it('should have the correct properties', () => {
@ -56,7 +57,7 @@ describe('BaseExtension', () => {
})
afterEach(() => {
jest.resetAllMocks()
vi.clearAllMocks()
})
it('should have the correct properties', () => {
@ -108,7 +109,7 @@ describe('BaseExtension', () => {
Object.defineProperty(global, 'localStorage', {
value: localStorageMock,
})
const mock = jest.spyOn(localStorage, 'setItem')
const mock = vi.spyOn(localStorage, 'setItem')
await baseExtension.registerSettings(settings)
expect(mock).toHaveBeenCalledWith(
@ -122,7 +123,7 @@ describe('BaseExtension', () => {
{ key: 'setting1', controllerProps: { value: 'value1' } } as any,
]
jest.spyOn(baseExtension, 'getSettings').mockResolvedValue(settings)
vi.spyOn(baseExtension, 'getSettings').mockResolvedValue(settings)
const value = await baseExtension.getSetting('setting1', 'defaultValue')
expect(value).toBe('value1')
@ -136,8 +137,8 @@ describe('BaseExtension', () => {
{ key: 'setting1', controllerProps: { value: 'value1' } } as any,
]
jest.spyOn(baseExtension, 'getSettings').mockResolvedValue(settings)
const mockSetItem = jest.spyOn(localStorage, 'setItem')
vi.spyOn(baseExtension, 'getSettings').mockResolvedValue(settings)
const mockSetItem = vi.spyOn(localStorage, 'setItem')
await baseExtension.updateSettings([
{ key: 'setting1', controllerProps: { value: 'newValue' } } as any,

View File

@ -1,4 +1,5 @@
import { it, expect } from 'vitest'
import { AssistantExtension } from './assistant';
import { ExtensionTypeEnum } from '../extension';

View File

@ -1,3 +1,4 @@
import { describe, it, test, expect, beforeEach } from 'vitest'
import { ConversationalExtension } from './conversational'
import { ExtensionTypeEnum } from '../extension'
import { Thread, ThreadAssistantInfo, ThreadMessage } from '../../types'

View File

@ -1,10 +1,11 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { AIEngine } from './AIEngine'
import { events } from '../../events'
import { ModelEvent, Model } from '../../../types'
jest.mock('../../events')
jest.mock('./EngineManager')
jest.mock('../../fs')
vi.mock('../../events')
vi.mock('./EngineManager')
vi.mock('../../fs')
class TestAIEngine extends AIEngine {
onUnload(): void {}
@ -52,7 +53,7 @@ describe('AIEngine', () => {
beforeEach(() => {
engine = new TestAIEngine('', '')
jest.clearAllMocks()
vi.clearAllMocks()
})
it('should load model successfully', async () => {

View File

@ -1,6 +1,4 @@
/**
* @jest-environment jsdom
*/
import { describe, it, test, expect, beforeEach } from 'vitest'
import { EngineManager } from './EngineManager'
import { AIEngine } from './AIEngine'
import { InferenceEngine } from '../../../types'

View File

@ -1,11 +1,9 @@
/**
* @jest-environment jsdom
*/
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest'
import { LocalOAIEngine } from './LocalOAIEngine'
import { events } from '../../events'
import { Model, ModelEvent } from '../../../types'
jest.mock('../../events')
vi.mock('../../events')
class TestLocalOAIEngine extends LocalOAIEngine {
inferenceUrl = 'http://test-local-inference-url'
@ -43,12 +41,12 @@ describe('LocalOAIEngine', () => {
beforeEach(() => {
engine = new TestLocalOAIEngine('', '')
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('onLoad', () => {
it('should call super.onLoad and subscribe to model events', () => {
const superOnLoadSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(engine)), 'onLoad')
const superOnLoadSpy = vi.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(engine)), 'onLoad')
engine.onLoad()
@ -64,11 +62,11 @@ describe('LocalOAIEngine', () => {
})
it('should load model when OnModelInit event is triggered', () => {
const loadModelSpy = jest.spyOn(engine, 'loadModel')
const loadModelSpy = vi.spyOn(engine, 'loadModel')
engine.onLoad()
// Get the event handler for OnModelInit
const onModelInitCall = (events.on as jest.Mock).mock.calls.find(
const onModelInitCall = (events.on as Mock).mock.calls.find(
call => call[0] === ModelEvent.OnModelInit
)
const onModelInitHandler = onModelInitCall[1]
@ -80,11 +78,11 @@ describe('LocalOAIEngine', () => {
})
it('should unload model when OnModelStop event is triggered', () => {
const unloadModelSpy = jest.spyOn(engine, 'unloadModel')
const unloadModelSpy = vi.spyOn(engine, 'unloadModel')
engine.onLoad()
// Get the event handler for OnModelStop
const onModelStopCall = (events.on as jest.Mock).mock.calls.find(
const onModelStopCall = (events.on as Mock).mock.calls.find(
call => call[0] === ModelEvent.OnModelStop
)
const onModelStopHandler = onModelStopCall[1]

View File

@ -1,6 +1,4 @@
/**
* @jest-environment jsdom
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { OAIEngine } from './OAIEngine'
import { events } from '../../events'
import {
@ -13,7 +11,7 @@ import {
ContentType,
} from '../../../types'
jest.mock('../../events')
vi.mock('../../events')
class TestOAIEngine extends OAIEngine {
inferenceUrl = 'http://test-inference-url'
@ -29,7 +27,7 @@ describe('OAIEngine', () => {
beforeEach(() => {
engine = new TestOAIEngine('', '')
jest.clearAllMocks()
vi.clearAllMocks()
})
it('should subscribe to events on load', () => {

View File

@ -1,6 +1,4 @@
/**
* @jest-environment jsdom
*/
import { describe, test, expect, beforeEach, vi } from 'vitest'
import { RemoteOAIEngine } from './'
class TestRemoteOAIEngine extends RemoteOAIEngine {
@ -16,8 +14,8 @@ describe('RemoteOAIEngine', () => {
})
test('should call onLoad and super.onLoad', () => {
const onLoadSpy = jest.spyOn(engine, 'onLoad')
const superOnLoadSpy = jest.spyOn(Object.getPrototypeOf(RemoteOAIEngine.prototype), 'onLoad')
const onLoadSpy = vi.spyOn(engine, 'onLoad')
const superOnLoadSpy = vi.spyOn(Object.getPrototypeOf(RemoteOAIEngine.prototype), 'onLoad')
engine.onLoad()
expect(onLoadSpy).toHaveBeenCalled()

View File

@ -1,6 +1,6 @@
import { expect } from '@jest/globals';
import { it, expect } from 'vitest'
import * as engines from './index'
it('should re-export all exports from ./AIEngine', () => {
expect(require('./index')).toHaveProperty('AIEngine');
});
expect(engines).toHaveProperty('AIEngine')
})

View File

@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest'
import { ConversationalExtension } from './index';
import { InferenceExtension } from './index';
import { AssistantExtension } from './index';

View File

@ -1,3 +1,4 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { MessageRequest, ThreadMessage } from '../../types'
import { BaseExtension, ExtensionTypeEnum } from '../extension'
import { InferenceExtension } from './'

View File

@ -1,21 +1,22 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { fs } from './fs'
describe('fs module', () => {
beforeEach(() => {
globalThis.core = {
api: {
writeFileSync: jest.fn(),
writeBlob: jest.fn(),
readFileSync: jest.fn(),
existsSync: jest.fn(),
readdirSync: jest.fn(),
mkdir: jest.fn(),
rm: jest.fn(),
unlinkSync: jest.fn(),
appendFileSync: jest.fn(),
copyFile: jest.fn(),
getGgufFiles: jest.fn(),
fileStat: jest.fn(),
writeFileSync: vi.fn(),
writeBlob: vi.fn(),
readFileSync: vi.fn(),
existsSync: vi.fn(),
readdirSync: vi.fn(),
mkdir: vi.fn(),
rm: vi.fn(),
unlinkSync: vi.fn(),
appendFileSync: vi.fn(),
copyFile: vi.fn(),
getGgufFiles: vi.fn(),
fileStat: vi.fn(),
},
}
})

View File

@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest'
import * as Core from './core'
import * as Events from './events'
import * as FileSystem from './fs'

View File

@ -1,10 +1,11 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { ModelManager } from './manager'
import { Model, ModelEvent } from '../../types'
import { events } from '../events'
jest.mock('../events', () => ({
vi.mock('../events', () => ({
events: {
emit: jest.fn(),
emit: vi.fn(),
},
}))
@ -20,7 +21,7 @@ describe('ModelManager', () => {
let mockModel: Model
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
;(global.window as any).core = {}
modelManager = new ModelManager()
mockModel = {

View File

@ -1,4 +1,5 @@
// web/utils/modelParam.test.ts
import { describe, it, expect } from 'vitest'
import {
normalizeValue,
validationRules,

View File

@ -1,4 +1,5 @@
import { it, expect } from 'vitest'
it('should declare global object core when importing the module and then deleting it', () => {
import('./index');

19
core/src/test/setup.ts Normal file
View File

@ -0,0 +1,19 @@
import { vi } from 'vitest'
// Ensure window exists in test environment
if (typeof window === 'undefined') {
global.window = {} as any
}
// Mock window.core for browser tests
if (!window.core) {
Object.defineProperty(window, 'core', {
value: {
engineManager: undefined
},
writable: true,
configurable: true
})
}
// Add any other global mocks needed for core tests

View File

@ -1,5 +1,6 @@
import { test, expect } from 'vitest'
import { NativeRoute } from '../index';
test('testNativeRouteEnum', () => {

View File

@ -1,4 +1,6 @@
import { it, expect } from 'vitest'
import { AssistantEvent } from './assistantEvent';
it('dummy test', () => { expect(true).toBe(true); });
it('should contain OnAssistantsUpdate event', () => {

View File

@ -1,8 +1,9 @@
import { AppConfigurationEventName } from './appConfigEvent';
describe('AppConfigurationEventName', () => {
import { describe, it, expect } from 'vitest'
import { AppConfigurationEventName } from './appConfigEvent';
describe('AppConfigurationEventName', () => {
it('should have the correct value for OnConfigurationUpdate', () => {
expect(AppConfigurationEventName.OnConfigurationUpdate).toBe('OnConfigurationUpdate');
});

View File

@ -1,4 +1,5 @@
import { test, expect } from 'vitest'
import * as assistant from './assistant';
import * as model from './model';
import * as thread from './thread';
@ -10,7 +11,7 @@ import * as miscellaneous from './miscellaneous';
import * as api from './api';
import * as setting from './setting';
test('test_module_exports', () => {
test('test_module_exports', () => {
expect(assistant).toBeDefined();
expect(model).toBeDefined();
expect(thread).toBeDefined();

View File

@ -1,8 +1,9 @@
import { ChatCompletionMessage, ChatCompletionRole } from './inferenceEntity';
test('test_chatCompletionMessage_withStringContent_andSystemRole', () => {
import { test, expect } from 'vitest'
import { ChatCompletionMessage, ChatCompletionRole } from './inferenceEntity';
test('test_chatCompletionMessage_withStringContent_andSystemRole', () => {
const message: ChatCompletionMessage = {
content: 'Hello, world!',
role: ChatCompletionRole.System,

View File

@ -1,7 +1,8 @@
import { InferenceEvent } from './inferenceEvent';
test('testInferenceEventEnumContainsOnInferenceStopped', () => {
import { test, expect } from 'vitest'
import { InferenceEvent } from './inferenceEvent';
test('testInferenceEventEnumContainsOnInferenceStopped', () => {
expect(InferenceEvent.OnInferenceStopped).toBe('OnInferenceStopped');
});

View File

@ -1,4 +1,5 @@
import { it, expect } from 'vitest'
import { MessageStatus } from './messageEntity';
it('should have correct values', () => {

View File

@ -1,7 +1,8 @@
import { MessageEvent } from './messageEvent';
test('testOnMessageSentValue', () => {
import { test, expect } from 'vitest'
import { MessageEvent } from './messageEvent';
test('testOnMessageSentValue', () => {
expect(MessageEvent.OnMessageSent).toBe('OnMessageSent');
});

View File

@ -1,7 +1,8 @@
import { MessageRequestType } from './messageRequestType';
test('testMessageRequestTypeEnumContainsThread', () => {
import { test, expect } from 'vitest'
import { MessageRequestType } from './messageRequestType';
test('testMessageRequestTypeEnumContainsThread', () => {
expect(MessageRequestType.Thread).toBe('Thread');
});

View File

@ -1,4 +1,5 @@
import { it, expect } from 'vitest'
import { SupportedPlatforms } from './systemResourceInfo';
it('should contain the correct values', () => {

View File

@ -1,3 +1,4 @@
import { test, expect } from 'vitest'
import { Model, ModelSettingParams, ModelRuntimeParams } from '../model'
import { InferenceEngine } from '../engine'

View File

@ -1,7 +1,8 @@
import { ModelEvent } from './modelEvent';
test('testOnModelInit', () => {
import { test, expect } from 'vitest'
import { ModelEvent } from './modelEvent';
test('testOnModelInit', () => {
expect(ModelEvent.OnModelInit).toBe('OnModelInit');
});

View File

@ -1,5 +1,6 @@
import { it, expect } from 'vitest'
import './index'
it('should not throw any errors', () => {
expect(() => require('./index')).not.toThrow();
});
expect(true).toBe(true)
})

View File

@ -1,7 +1,8 @@
import { it, expect } from 'vitest'
import * as SettingComponent from './settingComponent'
it('should not throw any errors when importing settingComponent', () => {
expect(() => require('./settingComponent')).not.toThrow()
expect(true).toBe(true)
})
it('should export SettingComponentProps type', () => {

View File

@ -1,10 +0,0 @@
const jestRunner = require('jest-runner');
class EmptyTestFileRunner extends jestRunner.default {
async runTests(tests, watcher, onStart, onResult, onFailure, options) {
const nonEmptyTests = tests.filter(test => test.context.hasteFS.getSize(test.path) > 0);
return super.runTests(nonEmptyTests, watcher, onStart, onResult, onFailure, options);
}
}
module.exports = EmptyTestFileRunner;

View File

@ -13,7 +13,7 @@
"declarationDir": "dist/types",
"outDir": "dist",
"importHelpers": true,
"types": ["jest", "node"]
"types": ["node"]
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]

22
core/vitest.config.ts Normal file
View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
reporter: ['text', 'json', 'html', 'lcov'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['node_modules/', 'dist/', 'src/**/*.test.ts']
},
include: ['src/**/*.test.ts'],
exclude: ['node_modules/', 'dist/']
},
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
}
})

View File

@ -1,5 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
}

View File

@ -7,7 +7,6 @@
"author": "Jan <service@jan.ai>",
"license": "MIT",
"scripts": {
"test": "jest",
"build": "rolldown -c rolldown.config.mjs",
"build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install"
},

View File

@ -1,3 +0,0 @@
module.exports = {
projects: ['<rootDir>/core'],
}

View File

@ -12,11 +12,10 @@
"lint": "yarn workspace @janhq/web-app lint",
"dev": "yarn dev:tauri",
"build": "yarn build:web && yarn build:tauri",
"test": "jest && yarn workspace @janhq/web-app test",
"test:coverage": "yarn test:coverage:jest && yarn test:coverage:vitest && yarn merge:coverage",
"test:coverage:jest": "jest --coverage --coverageDirectory=coverage/jest",
"test:coverage:vitest": "yarn workspace @janhq/web-app test:coverage",
"merge:coverage": "node scripts/merge-coverage.js",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:prepare": "yarn build:icon && yarn copy:assets:tauri && yarn build --no-bundle ",
"test:e2e:linux": "yarn test:prepare && xvfb-run yarn workspace tests-e2-js test",
"test:e2e:win32": "yarn test:prepare && yarn workspace tests-e2-js test",
@ -39,21 +38,19 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.5",
"@vitest/coverage-v8": "^3.1.3",
"concurrently": "^9.1.0",
"cpx": "^1.5.0",
"cross-env": "^7.0.3",
"happy-dom": "^15.11.6",
"husky": "^9.1.5",
"istanbul-api": "^3.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.1.7",
"jest": "^30.0.3",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^26.1.0",
"nyc": "^17.1.0",
"rimraf": "^3.0.2",
"run-script-os": "^1.1.6",
"tar": "^4.4.19",
"unzipper": "^0.12.3",
"vitest": "^3.1.3",
"wait-on": "^7.0.1"
},
"version": "0.0.0",

View File

@ -1,145 +0,0 @@
const { createCoverageMap } = require('istanbul-lib-coverage')
const { createReporter } = require('istanbul-api')
const fs = require('fs')
const path = require('path')
const coverageDir = path.join(__dirname, '../coverage')
const jestCoverage = path.join(coverageDir, 'jest/coverage-final.json')
const vitestCoverage = path.join(coverageDir, 'vitest/coverage-final.json')
const mergedDir = path.join(coverageDir, 'merged')
function normalizePath(filePath, workspace) {
if (workspace === 'jest') {
return `[CORE] ${filePath}`
} else if (workspace === 'vitest') {
return `[WEB-APP] ${filePath}`
}
return filePath
}
async function mergeCoverage() {
const map = createCoverageMap({})
console.log('🔍 Checking coverage files...')
console.log('Jest coverage path:', jestCoverage)
console.log('Vitest coverage path:', vitestCoverage)
console.log('Jest file exists:', fs.existsSync(jestCoverage))
console.log('Vitest file exists:', fs.existsSync(vitestCoverage))
// Load Jest coverage (core workspace)
if (fs.existsSync(jestCoverage)) {
const jestData = JSON.parse(fs.readFileSync(jestCoverage, 'utf8'))
console.log('Jest data keys:', Object.keys(jestData).length)
map.merge(jestData)
console.log('✓ Merged Jest coverage (core workspace)')
} else {
console.log('❌ Jest coverage file not found')
}
// Load Vitest coverage (web-app workspace)
if (fs.existsSync(vitestCoverage)) {
const vitestData = JSON.parse(fs.readFileSync(vitestCoverage, 'utf8'))
console.log('Vitest data keys:', Object.keys(vitestData).length)
map.merge(vitestData)
console.log('✓ Merged Vitest coverage (web-app workspace)')
} else {
console.log('❌ Vitest coverage file not found')
}
console.log('📊 Total files in coverage map:', map.files().length)
// Create merged directory
if (!fs.existsSync(mergedDir)) {
fs.mkdirSync(mergedDir, { recursive: true })
console.log('✓ Created merged directory')
}
try {
console.log('🔄 Generating reports...')
const context = require('istanbul-lib-report').createContext({
dir: mergedDir,
coverageMap: map,
})
const htmlReporter = require('istanbul-reports').create('html')
const lcovReporter = require('istanbul-reports').create('lcov')
const textReporter = require('istanbul-reports').create('text')
// Generate reports
htmlReporter.execute(context)
lcovReporter.execute(context)
textReporter.execute(context)
console.log('\n📊 Coverage reports merged successfully!')
console.log('📁 HTML report: coverage/merged/index.html')
console.log('📁 LCOV report: coverage/merged/lcov.info')
// Check if files were created
if (fs.existsSync(mergedDir)) {
const mergedFiles = fs.readdirSync(mergedDir)
console.log('📁 Files in merged directory:', mergedFiles)
}
} catch (error) {
console.error('❌ Error generating reports:', error.message)
console.error('Stack trace:', error.stack)
throw error
}
// Generate separate reports for each workspace
await generateWorkspaceReports()
}
async function generateWorkspaceReports() {
// Generate separate core report
if (fs.existsSync(jestCoverage)) {
const coreMap = createCoverageMap({})
const jestData = JSON.parse(fs.readFileSync(jestCoverage, 'utf8'))
coreMap.merge(jestData)
const coreDir = path.join(coverageDir, 'core-only')
if (!fs.existsSync(coreDir)) {
fs.mkdirSync(coreDir, { recursive: true })
}
const coreContext = require('istanbul-lib-report').createContext({
dir: coreDir,
coverageMap: coreMap,
})
const htmlReporter = require('istanbul-reports').create('html')
const textSummaryReporter =
require('istanbul-reports').create('text-summary')
htmlReporter.execute(coreContext)
textSummaryReporter.execute(coreContext)
console.log('📁 Core-only report: coverage/core-only/index.html')
}
// Generate separate web-app report
if (fs.existsSync(vitestCoverage)) {
const webAppMap = createCoverageMap({})
const vitestData = JSON.parse(fs.readFileSync(vitestCoverage, 'utf8'))
webAppMap.merge(vitestData)
const webAppDir = path.join(coverageDir, 'web-app-only')
if (!fs.existsSync(webAppDir)) {
fs.mkdirSync(webAppDir, { recursive: true })
}
const webAppContext = require('istanbul-lib-report').createContext({
dir: webAppDir,
coverageMap: webAppMap,
})
const htmlReporter = require('istanbul-reports').create('html')
const textSummaryReporter =
require('istanbul-reports').create('text-summary')
htmlReporter.execute(webAppContext)
textSummaryReporter.execute(webAppContext)
console.log('📁 Web-app-only report: coverage/web-app-only/index.html')
}
}
mergeCoverage().catch(console.error)

View File

@ -20,8 +20,6 @@ fi
# bundle additional resources in the AppDir without pulling in their dependencies
cp ./src-tauri/resources/bin/bun $APP_DIR/usr/bin/bun
mkdir -p $LIB_DIR/engines
cp -f ./src-tauri/binaries/deps/*.so* $LIB_DIR/
cp -f ./src-tauri/binaries/*.so* $LIB_DIR/
# remove appimage generated by tauri build
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1)

View File

@ -11,7 +11,6 @@
"deb": {
"files": {
"usr/bin/bun": "resources/bin/bun",
"usr/lib/Jan/binaries": "binaries/deps",
"usr/lib/Jan/resources/lib/libvulkan.so": "resources/lib/libvulkan.so"
}
}

13
vitest.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
// Core package - use its own vitest config
'./core',
// Web-app package - use its own vitest config
'./web-app'
]
}
})

View File

@ -1,10 +1,25 @@
import { expect, afterEach } from 'vitest'
import { expect, afterEach, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
// extends Vitest's expect method with methods from react-testing-library
expect.extend(matchers)
// Mock window.matchMedia for useMediaQuery tests
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// runs a cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
cleanup()

View File

@ -9,6 +9,11 @@ export default defineConfig({
setupFiles: ['./src/test/setup.ts'],
globals: true,
css: true,
coverage: {
reporter: ['text', 'json', 'html', 'lcov'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['node_modules/', 'dist/', 'src/**/*.test.ts', 'src/**/*.test.tsx', 'src/test/**/*']
},
},
resolve: {
alias: {