test: more unit tests (#5130)

* test: more unit tests

- Adjust vitest configuration to handle main process and renderer process tests separately
- Add unit tests for main process utils
- Add unit tests for the renderer process
- Add three component tests to verify vitest usage: `DragableList`, `Scrollbar`, `QuickPanelView`
- Add an e2e startup test to verify playwright usage
- Extract `splitApiKeyString` and add tests for it
- Add and format some comments

* fix: mock individual properties

* test: add tests for CustomTag

* test: add tests for ExpandableText

* test: conditional rendering tooltip of tag

* chore: update dependencies
This commit is contained in:
one 2025-05-26 16:50:26 +08:00 committed by GitHub
parent a05a7e45cc
commit 665a62080b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 2366 additions and 481 deletions

7
.gitignore vendored
View File

@ -47,8 +47,13 @@ local
.cursorrules
.cursor/rules
# test
# vitest
coverage
.vitest-cache
vitest.config.*.timestamp-*
# playwright
playwright-report
test-results
YOUR_MEMORY_FILE_PATH

View File

@ -45,12 +45,13 @@
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"check:i18n": "node scripts/check-i18n.js",
"test": "yarn test:renderer",
"test:coverage": "yarn test:renderer:coverage",
"test:node": "npx -y tsx --test src/**/*.test.ts",
"test:renderer": "vitest run",
"test:renderer:ui": "vitest --ui",
"test:renderer:coverage": "vitest run --coverage",
"test": "vitest run --silent",
"test:main": "vitest run --project main",
"test:renderer": "vitest run --project renderer",
"test:coverage": "vitest run --coverage --silent",
"test:ui": "vitest --ui",
"test:watch": "vitest",
"test:e2e": "yarn playwright test",
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
@ -118,9 +119,13 @@
"@modelcontextprotocol/sdk": "^1.11.4",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@playwright/test": "^1.52.0",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.4.2",
"@swc/plugin-styled-components": "^7.1.5",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@tryfabric/martian": "^1.2.4",
"@types/diff": "^7",
"@types/fs-extra": "^11",
@ -139,8 +144,10 @@
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/ui": "^3.1.1",
"@vitest/web-worker": "^3.1.3",
"@vitest/browser": "^3.1.4",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.1.4",
"@vitest/web-worker": "^3.1.4",
"@xyflow/react": "^12.4.4",
"antd": "^5.22.5",
"axios": "^1.7.3",
@ -165,6 +172,7 @@
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"i18next": "^23.11.5",
"jest-styled-components": "^7.2.0",
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
@ -175,6 +183,7 @@
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"p-queue": "^8.1.0",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
"rc-virtual-list": "^3.18.6",
"react": "^19.0.0",
@ -206,7 +215,7 @@
"typescript": "^5.6.2",
"uuid": "^10.0.0",
"vite": "6.2.6",
"vitest": "^3.1.1"
"vitest": "^3.1.4"
},
"resolutions": {
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",

42
playwright.config.ts Normal file
View File

@ -0,0 +1,42 @@
import { defineConfig, devices } from '@playwright/test'
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
// Look for test files, relative to this configuration file.
testDir: './tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
})

View File

@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { decrypt, encrypt } from '../aes'
const key = '12345678901234567890123456789012' // 32字节
const iv = '1234567890abcdef1234567890abcdef' // 32字节hex实际应16字节hex
function getIv16() {
// 取前16字节作为 hex
return iv.slice(0, 32)
}
describe('aes utils', () => {
it('should encrypt and decrypt normal string', () => {
const text = 'hello world'
const { iv: outIv, encryptedData } = encrypt(text, key, getIv16())
expect(typeof encryptedData).toBe('string')
expect(outIv).toBe(getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should support unicode and special chars', () => {
const text = '你好,世界!🌟🚀'
const { encryptedData } = encrypt(text, key, getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should handle empty string', () => {
const text = ''
const { encryptedData } = encrypt(text, key, getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should encrypt and decrypt long string', () => {
const text = 'a'.repeat(100_000)
const { encryptedData } = encrypt(text, key, getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should throw error for wrong key', () => {
const text = 'test'
const { encryptedData } = encrypt(text, key, getIv16())
expect(() => decrypt(encryptedData, getIv16(), 'wrongkeywrongkeywrongkeywrongkey')).toThrow()
})
it('should throw error for wrong iv', () => {
const text = 'test'
const { encryptedData } = encrypt(text, key, getIv16())
expect(() => decrypt(encryptedData, 'abcdefabcdefabcdefabcdefabcdefab', key)).toThrow()
})
it('should throw error for invalid key/iv length', () => {
expect(() => encrypt('test', 'shortkey', getIv16())).toThrow()
expect(() => encrypt('test', key, 'shortiv')).toThrow()
})
it('should throw error for invalid encrypted data', () => {
expect(() => decrypt('nothexdata', getIv16(), key)).toThrow()
})
it('should throw error for non-string input', () => {
// @ts-expect-error purposely pass wrong type to test error branch
expect(() => encrypt(null, key, getIv16())).toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
expect(() => decrypt(null, getIv16(), key)).toThrow()
})
})

View File

@ -0,0 +1,243 @@
import * as fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { FileTypes } from '@types'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getAllFiles, getAppConfigDir, getConfigDir, getFilesDir, getFileType, getTempDir } from '../file'
// Mock dependencies
vi.mock('node:fs')
vi.mock('node:os')
vi.mock('node:path')
vi.mock('uuid', () => ({
v4: () => 'mock-uuid'
}))
vi.mock('electron', () => ({
app: {
getPath: vi.fn((key) => {
if (key === 'temp') return '/mock/temp'
if (key === 'userData') return '/mock/userData'
return '/mock/unknown'
})
}
}))
describe('file', () => {
beforeEach(() => {
vi.clearAllMocks()
// Mock path.extname
vi.mocked(path.extname).mockImplementation((file) => {
const parts = file.split('.')
return parts.length > 1 ? `.${parts[parts.length - 1]}` : ''
})
// Mock path.basename
vi.mocked(path.basename).mockImplementation((file) => {
const parts = file.split('/')
return parts[parts.length - 1]
})
// Mock path.join
vi.mocked(path.join).mockImplementation((...args) => args.join('/'))
// Mock os.homedir
vi.mocked(os.homedir).mockReturnValue('/mock/home')
})
afterEach(() => {
vi.resetAllMocks()
})
describe('getFileType', () => {
it('should return IMAGE for image extensions', () => {
expect(getFileType('.jpg')).toBe(FileTypes.IMAGE)
expect(getFileType('.jpeg')).toBe(FileTypes.IMAGE)
expect(getFileType('.png')).toBe(FileTypes.IMAGE)
expect(getFileType('.gif')).toBe(FileTypes.IMAGE)
expect(getFileType('.webp')).toBe(FileTypes.IMAGE)
expect(getFileType('.bmp')).toBe(FileTypes.IMAGE)
})
it('should return VIDEO for video extensions', () => {
expect(getFileType('.mp4')).toBe(FileTypes.VIDEO)
expect(getFileType('.avi')).toBe(FileTypes.VIDEO)
expect(getFileType('.mov')).toBe(FileTypes.VIDEO)
expect(getFileType('.mkv')).toBe(FileTypes.VIDEO)
expect(getFileType('.flv')).toBe(FileTypes.VIDEO)
})
it('should return AUDIO for audio extensions', () => {
expect(getFileType('.mp3')).toBe(FileTypes.AUDIO)
expect(getFileType('.wav')).toBe(FileTypes.AUDIO)
expect(getFileType('.ogg')).toBe(FileTypes.AUDIO)
expect(getFileType('.flac')).toBe(FileTypes.AUDIO)
expect(getFileType('.aac')).toBe(FileTypes.AUDIO)
})
it('should return TEXT for text extensions', () => {
expect(getFileType('.txt')).toBe(FileTypes.TEXT)
expect(getFileType('.md')).toBe(FileTypes.TEXT)
expect(getFileType('.html')).toBe(FileTypes.TEXT)
expect(getFileType('.json')).toBe(FileTypes.TEXT)
expect(getFileType('.js')).toBe(FileTypes.TEXT)
expect(getFileType('.ts')).toBe(FileTypes.TEXT)
expect(getFileType('.css')).toBe(FileTypes.TEXT)
expect(getFileType('.java')).toBe(FileTypes.TEXT)
expect(getFileType('.py')).toBe(FileTypes.TEXT)
})
it('should return DOCUMENT for document extensions', () => {
expect(getFileType('.pdf')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.pptx')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.docx')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.xlsx')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.odt')).toBe(FileTypes.DOCUMENT)
})
it('should return OTHER for unknown extensions', () => {
expect(getFileType('.unknown')).toBe(FileTypes.OTHER)
expect(getFileType('')).toBe(FileTypes.OTHER)
expect(getFileType('.')).toBe(FileTypes.OTHER)
expect(getFileType('...')).toBe(FileTypes.OTHER)
expect(getFileType('.123')).toBe(FileTypes.OTHER)
})
it('should handle case-insensitive extensions', () => {
expect(getFileType('.JPG')).toBe(FileTypes.IMAGE)
expect(getFileType('.PDF')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.Mp3')).toBe(FileTypes.AUDIO)
expect(getFileType('.HtMl')).toBe(FileTypes.TEXT)
expect(getFileType('.Xlsx')).toBe(FileTypes.DOCUMENT)
})
it('should handle extensions without leading dot', () => {
expect(getFileType('jpg')).toBe(FileTypes.OTHER)
expect(getFileType('pdf')).toBe(FileTypes.OTHER)
expect(getFileType('mp3')).toBe(FileTypes.OTHER)
})
it('should handle extreme cases', () => {
expect(getFileType('.averylongfileextensionname')).toBe(FileTypes.OTHER)
expect(getFileType('.tar.gz')).toBe(FileTypes.OTHER)
expect(getFileType('.文件')).toBe(FileTypes.OTHER)
expect(getFileType('.файл')).toBe(FileTypes.OTHER)
})
})
describe('getAllFiles', () => {
it('should return all valid files recursively', () => {
// Mock file system
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockImplementation((dirPath) => {
if (dirPath === '/test') {
return ['file1.txt', 'file2.pdf', 'subdir']
} else if (dirPath === '/test/subdir') {
return ['file3.md', 'file4.docx']
}
return []
})
vi.mocked(fs.statSync).mockImplementation((filePath) => {
const isDir = String(filePath).endsWith('subdir')
return {
isDirectory: () => isDir,
size: 1024
} as fs.Stats
})
const result = getAllFiles('/test')
expect(result).toHaveLength(4)
expect(result[0].id).toBe('mock-uuid')
expect(result[0].name).toBe('file1.txt')
expect(result[0].type).toBe(FileTypes.TEXT)
expect(result[1].name).toBe('file2.pdf')
expect(result[1].type).toBe(FileTypes.DOCUMENT)
})
it('should skip hidden files', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockReturnValue(['.hidden', 'visible.txt'])
vi.mocked(fs.statSync).mockReturnValue({
isDirectory: () => false,
size: 1024
} as fs.Stats)
const result = getAllFiles('/test')
expect(result).toHaveLength(1)
expect(result[0].name).toBe('visible.txt')
})
it('should skip unsupported file types', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockReturnValue(['image.jpg', 'video.mp4', 'audio.mp3', 'document.pdf'])
vi.mocked(fs.statSync).mockReturnValue({
isDirectory: () => false,
size: 1024
} as fs.Stats)
const result = getAllFiles('/test')
// Should only include document.pdf as the others are excluded types
expect(result).toHaveLength(1)
expect(result[0].name).toBe('document.pdf')
expect(result[0].type).toBe(FileTypes.DOCUMENT)
})
it('should return empty array for empty directory', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockReturnValue([])
const result = getAllFiles('/empty')
expect(result).toHaveLength(0)
})
it('should handle file system errors', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockImplementation(() => {
throw new Error('Directory not found')
})
// Since the function doesn't have error handling, we expect it to propagate
expect(() => getAllFiles('/nonexistent')).toThrow('Directory not found')
})
})
describe('getTempDir', () => {
it('should return correct temp directory path', () => {
const tempDir = getTempDir()
expect(tempDir).toBe('/mock/temp/CherryStudio')
})
})
describe('getFilesDir', () => {
it('should return correct files directory path', () => {
const filesDir = getFilesDir()
expect(filesDir).toBe('/mock/userData/Data/Files')
})
})
describe('getConfigDir', () => {
it('should return correct config directory path', () => {
const configDir = getConfigDir()
expect(configDir).toBe('/mock/home/.cherrystudio/config')
})
})
describe('getAppConfigDir', () => {
it('should return correct app config directory path', () => {
const appConfigDir = getAppConfigDir('test-app')
expect(appConfigDir).toBe('/mock/home/.cherrystudio/config/test-app')
})
it('should handle empty app name', () => {
const appConfigDir = getAppConfigDir('')
expect(appConfigDir).toBe('/mock/home/.cherrystudio/config/')
})
})
})

View File

@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest'
import { compress, decompress } from '../zip'
const jsonStr = JSON.stringify({ foo: 'bar', num: 42, arr: [1, 2, 3] })
// 辅助函数:生成大字符串
function makeLargeString(size: number) {
return 'a'.repeat(size)
}
describe('zip', () => {
describe('compress & decompress', () => {
it('should compress and decompress a normal JSON string', async () => {
const compressed = await compress(jsonStr)
expect(compressed).toBeInstanceOf(Buffer)
const decompressed = await decompress(compressed)
expect(decompressed).toBe(jsonStr)
})
it('should handle empty string', async () => {
const compressed = await compress('')
expect(compressed).toBeInstanceOf(Buffer)
const decompressed = await decompress(compressed)
expect(decompressed).toBe('')
})
it('should handle large string', async () => {
const largeStr = makeLargeString(100_000)
const compressed = await compress(largeStr)
expect(compressed).toBeInstanceOf(Buffer)
expect(compressed.length).toBeLessThan(largeStr.length)
const decompressed = await decompress(compressed)
expect(decompressed).toBe(largeStr)
})
it('should throw error when decompressing invalid buffer', async () => {
const invalidBuffer = Buffer.from('not a valid gzip', 'utf-8')
await expect(decompress(invalidBuffer)).rejects.toThrow()
})
it('should throw error when compress input is not string', async () => {
// @ts-expect-error purposely pass wrong type to test error branch
await expect(compress(null)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(compress(undefined)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(compress(123)).rejects.toThrow()
})
it('should throw error when decompress input is not buffer', async () => {
// @ts-expect-error purposely pass wrong type to test error branch
await expect(decompress(null)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(decompress(undefined)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(decompress('string')).rejects.toThrow()
})
})
})

View File

@ -9,10 +9,10 @@ const gunzipPromise = util.promisify(zlib.gunzip)
/**
*
* @param {string} str JSON
* @returns {Promise<Buffer>} Buffer
* @param str
*/
export async function compress(str) {
export async function compress(str: string): Promise<Buffer> {
try {
const buffer = Buffer.from(str, 'utf-8')
return await gzipPromise(buffer)
@ -27,7 +27,7 @@ export async function compress(str) {
* @param {Buffer} compressedBuffer - Buffer
* @returns {Promise<string>} JSON
*/
export async function decompress(compressedBuffer) {
export async function decompress(compressedBuffer: Buffer): Promise<string> {
try {
const buffer = await gunzipPromise(compressedBuffer)
return buffer.toString('utf-8')

View File

@ -1,49 +0,0 @@
import { vi } from 'vitest'
vi.mock('electron-log/renderer', () => {
return {
default: {
info: console.log,
error: console.error,
warn: console.warn,
debug: console.debug,
verbose: console.log,
silly: console.log,
log: console.log,
transports: {
console: {
level: 'info'
}
}
}
}
})
vi.stubGlobal('window', {
electron: {
ipcRenderer: {
on: vi.fn(), // Mocking ipcRenderer.on
send: vi.fn() // Mocking ipcRenderer.send
}
},
api: {
file: {
read: vi.fn().mockResolvedValue('[]'), // Mock file.read to return an empty array (you can customize this)
writeWithId: vi.fn().mockResolvedValue(undefined) // Mock file.writeWithId to do nothing
}
}
})
vi.mock('axios', () => ({
default: {
get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request
post: vi.fn().mockResolvedValue({ data: {} }) // Mocking axios POST request
// You can add other axios methods like put, delete etc. as needed
}
}))
vi.stubGlobal('window', {
...global.window, // Copy other global properties
addEventListener: vi.fn(), // Mock addEventListener
removeEventListener: vi.fn() // You can also mock removeEventListener if needed
})

View File

@ -434,7 +434,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
$pageSize={ctx.pageSize}
$selectedColor={selectedColor}
$selectedColorHover={selectedColorHover}
className={ctx.isVisible ? 'visible' : ''}>
className={ctx.isVisible ? 'visible' : ''}
data-testid="quick-panel">
<QuickPanelBody
ref={bodyRef}
onMouseMove={() =>

View File

@ -3,12 +3,12 @@ import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onScroll'> {
right?: boolean
ref?: React.RefObject<HTMLDivElement | null>
right?: boolean
onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll
}
const Scrollbar: FC<Props> = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => {
const Scrollbar: FC<Props> = ({ ref: passedRef, right, children, onScroll: externalOnScroll, ...htmlProps }) => {
const [isScrolling, setIsScrolling] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
@ -43,7 +43,8 @@ const Scrollbar: FC<Props> = ({ ref: passedRef, children, onScroll: externalOnSc
return (
<Container
{...htmlProps} // Pass other HTML attributes
isScrolling={isScrolling}
$isScrolling={isScrolling}
$right={right}
onScroll={combinedOnScroll} // Use the combined handler
ref={passedRef}>
{children}
@ -51,15 +52,15 @@ const Scrollbar: FC<Props> = ({ ref: passedRef, children, onScroll: externalOnSc
)
}
const Container = styled.div<{ isScrolling: boolean; right?: boolean }>`
const Container = styled.div<{ $isScrolling: boolean; $right?: boolean }>`
overflow-y: auto;
&::-webkit-scrollbar-thumb {
transition: background 2s ease;
background: ${(props) =>
props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''})` : 'transparent'};
props.$isScrolling ? `var(--color-scrollbar-thumb${props.$right ? '-right' : ''})` : 'transparent'};
&:hover {
background: ${(props) =>
props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''}-hover)` : 'transparent'};
props.$isScrolling ? `var(--color-scrollbar-thumb${props.$right ? '-right' : ''}-hover)` : 'transparent'};
}
}
`

View File

@ -0,0 +1,44 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import CustomTag from '../CustomTag'
const COLOR = '#ff0000'
describe('CustomTag', () => {
it('should render children text', () => {
render(<CustomTag color={COLOR}>content</CustomTag>)
expect(screen.getByText('content')).toBeInTheDocument()
})
it('should render icon if provided', () => {
render(
<CustomTag color={COLOR} icon={<span data-testid="icon">cherry</span>}>
content
</CustomTag>
)
expect(screen.getByTestId('icon')).toBeInTheDocument()
expect(screen.getByText('content')).toBeInTheDocument()
})
it('should show tooltip if tooltip prop is set', async () => {
render(
<CustomTag color={COLOR} tooltip="reasoning model">
reasoning
</CustomTag>
)
// 鼠标悬停触发 Tooltip
await userEvent.hover(screen.getByText('reasoning'))
expect(await screen.findByText('reasoning model')).toBeInTheDocument()
})
it('should not render Tooltip when tooltip is not set', () => {
render(<CustomTag color="#ff0000">no tooltip</CustomTag>)
expect(screen.getByText('no tooltip')).toBeInTheDocument()
// 不应有 tooltip 相关内容
expect(document.querySelector('.ant-tooltip')).toBeNull()
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,282 @@
/// <reference types="@vitest/browser/context" />
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import DragableList from '../DragableList'
// mock @hello-pangea/dnd 组件
vi.mock('@hello-pangea/dnd', () => {
return {
__esModule: true,
DragDropContext: ({ children, onDragEnd }: any) => {
// 挂载到 window 以便测试用例直接调用
window.triggerOnDragEnd = (result = { source: { index: 0 }, destination: { index: 1 } }, provided = {}) => {
onDragEnd && onDragEnd(result, provided)
}
return <div data-testid="drag-drop-context">{children}</div>
},
Droppable: ({ children }: any) => (
<div data-testid="droppable">
{children({ droppableProps: {}, innerRef: () => {}, placeholder: <div data-testid="placeholder" /> })}
</div>
),
Draggable: ({ children, draggableId, index }: any) => (
<div data-testid={`draggable-${draggableId}-${index}`}>
{children({ draggableProps: {}, dragHandleProps: {}, innerRef: () => {} })}
</div>
)
}
})
// mock VirtualList 只做简单渲染
vi.mock('rc-virtual-list', () => ({
__esModule: true,
default: ({ data, itemKey, children }: any) => (
<div data-testid="virtual-list">
{data.map((item: any, idx: number) => (
<div key={item[itemKey] || item} data-testid="virtual-list-item">
{children(item, idx)}
</div>
))}
</div>
)
}))
declare global {
interface Window {
triggerOnDragEnd: (result?: any, provided?: any) => void
}
}
describe('DragableList', () => {
describe('rendering', () => {
it('should render all list items', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
render(
<DragableList list={list} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
const items = screen.getAllByTestId('item')
expect(items.length).toBe(3)
expect(items[0].textContent).toBe('A')
expect(items[1].textContent).toBe('B')
expect(items[2].textContent).toBe('C')
})
it('should render with custom style and listStyle', () => {
const list = [{ id: 'a', name: 'A' }]
const style = { background: 'red' }
const listStyle = { color: 'blue' }
render(
<DragableList list={list} style={style} listStyle={listStyle} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// 检查 style 是否传递到外层容器
const virtualList = screen.getByTestId('virtual-list')
expect(virtualList.parentElement).toHaveStyle({ background: 'red' })
})
it('should render nothing when list is empty', () => {
render(
<DragableList list={[]} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// 虚拟列表存在但无内容
const items = screen.queryAllByTestId('item')
expect(items.length).toBe(0)
})
})
describe('drag and drop', () => {
it('should call onUpdate with new order after drag end', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
const newOrder = [list[1], list[2], list[0]]
const onUpdate = vi.fn()
render(
<DragableList list={list} onUpdate={onUpdate}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// 直接调用 window.triggerOnDragEnd 模拟拖拽结束
window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 2 } }, {})
expect(onUpdate).toHaveBeenCalledWith(newOrder)
expect(onUpdate).toHaveBeenCalledTimes(1)
})
it('should call onDragStart and onDragEnd', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
const onDragStart = vi.fn()
const onDragEnd = vi.fn()
render(
<DragableList list={list} onUpdate={() => {}} onDragStart={onDragStart} onDragEnd={onDragEnd}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// 先手动调用 onDragStart
onDragStart()
// 再模拟拖拽结束
window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 1 } }, {})
expect(onDragStart).toHaveBeenCalledTimes(1)
expect(onDragEnd).toHaveBeenCalledTimes(1)
})
it('should not call onUpdate if dropped at same position', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
const onUpdate = vi.fn()
render(
<DragableList list={list} onUpdate={onUpdate}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// 模拟拖拽到自身
window.triggerOnDragEnd({ source: { index: 1 }, destination: { index: 1 } }, {})
expect(onUpdate).toHaveBeenCalledTimes(1)
expect(onUpdate.mock.calls[0][0]).toEqual(list)
})
})
describe('edge cases', () => {
it('should work with single item', () => {
const list = [{ id: 'a', name: 'A' }]
const onUpdate = vi.fn()
render(
<DragableList list={list} onUpdate={onUpdate}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// 拖拽自身
window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 0 } }, {})
expect(onUpdate).toHaveBeenCalledTimes(1)
expect(onUpdate.mock.calls[0][0]).toEqual(list)
})
it('should not crash if callbacks are undefined', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' }
]
// 不传 onDragStart/onDragEnd
expect(() => {
render(
<DragableList list={list} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 1 } }, {})
}).not.toThrow()
})
it('should handle items without id', () => {
const list = ['A', 'B', 'C']
const onUpdate = vi.fn()
render(
<DragableList list={list} onUpdate={onUpdate}>
{(item) => <div data-testid="item">{item}</div>}
</DragableList>
)
// 拖拽第0项到第2项
window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 2 } }, {})
expect(onUpdate).toHaveBeenCalledTimes(1)
expect(onUpdate.mock.calls[0][0]).toEqual(['B', 'C', 'A'])
})
})
describe('interaction', () => {
it('should show placeholder during drag', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
render(
<DragableList list={list} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// placeholder 应该在初始渲染时就存在
const placeholder = screen.getByTestId('placeholder')
expect(placeholder).toBeInTheDocument()
})
it('should reorder correctly when dragged to first/last', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
const onUpdate = vi.fn()
render(
<DragableList list={list} onUpdate={onUpdate}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
// 拖拽第2项到第0项
window.triggerOnDragEnd({ source: { index: 2 }, destination: { index: 0 } }, {})
expect(onUpdate).toHaveBeenCalledWith([
{ id: 'c', name: 'C' },
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' }
])
// 拖拽第0项到第2项
onUpdate.mockClear()
window.triggerOnDragEnd({ source: { index: 0 }, destination: { index: 2 } }, {})
expect(onUpdate).toHaveBeenCalledWith([
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' },
{ id: 'a', name: 'A' }
])
})
})
describe('snapshot', () => {
it('should match snapshot', () => {
const list = [
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]
const { container } = render(
<DragableList list={list} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DragableList>
)
expect(container).toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,33 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import ExpandableText from '../ExpandableText'
// mock i18n
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (k: string) => k })
}))
describe('ExpandableText', () => {
const TEXT = 'This is a long text for testing.'
it('should render text and expand button', () => {
render(<ExpandableText text={TEXT} />)
expect(screen.getByText(TEXT)).toBeInTheDocument()
expect(screen.getByRole('button')).toHaveTextContent('common.expand')
})
it('should toggle expand/collapse when button is clicked', async () => {
render(<ExpandableText text={TEXT} />)
const button = screen.getByRole('button')
// 初始为收起状态
expect(button).toHaveTextContent('common.expand')
// 点击展开
await userEvent.click(button)
expect(button).toHaveTextContent('common.collapse')
// 再次点击收起
await userEvent.click(button)
expect(button).toHaveTextContent('common.expand')
})
})

View File

@ -0,0 +1,188 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useEffect } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { QuickPanelListItem, QuickPanelProvider, QuickPanelView, useQuickPanel } from '../QuickPanel'
function createList(length: number, prefix = 'Item', extra: Partial<QuickPanelListItem> = {}) {
return Array.from({ length }, (_, i) => ({
label: `${prefix} ${i + 1}`,
description: `${prefix} Description ${i + 1}`,
icon: `${prefix} Icon ${i + 1}`,
action: () => {},
...extra
}))
}
type KeyStep = {
key: string
ctrlKey?: boolean
expected: string | ((text: string) => boolean)
}
const PAGE_SIZE = 7
// 用于测试 open 行为的组件
function OpenPanelOnMount({ list }: { list: QuickPanelListItem[] }) {
const quickPanel = useQuickPanel()
useEffect(() => {
quickPanel.open({
title: 'Test Panel',
list,
symbol: 'test',
pageSize: PAGE_SIZE
})
}, [list, quickPanel])
return null
}
describe('QuickPanelView', () => {
beforeEach(() => {
// 添加一个假的 .inputbar textarea 到 document.body
const inputbar = document.createElement('div')
inputbar.className = 'inputbar'
const textarea = document.createElement('textarea')
inputbar.appendChild(textarea)
document.body.appendChild(inputbar)
})
afterEach(() => {
const inputbar = document.querySelector('.inputbar')
if (inputbar) inputbar.remove()
})
describe('rendering', () => {
it('should render without crashing when wrapped in QuickPanelProvider', () => {
render(
<QuickPanelProvider>
<QuickPanelView setInputText={vi.fn()} />
</QuickPanelProvider>
)
// 检查面板容器是否存在且初始不可见
const panel = screen.getByTestId('quick-panel')
expect(panel.classList.contains('visible')).toBe(false)
})
it('should render list after open', async () => {
const list = createList(100)
render(
<QuickPanelProvider>
<QuickPanelView setInputText={vi.fn()} />
<OpenPanelOnMount list={list} />
</QuickPanelProvider>
)
// 检查面板可见
const panel = screen.getByTestId('quick-panel')
expect(panel.classList.contains('visible')).toBe(true)
// 检查第一个 item 是否渲染
expect(screen.getByText('Item 1')).toBeInTheDocument()
})
})
describe('focusing', () => {
// 执行一系列按键,检查 focused item 是否正确
async function runKeySequenceAndCheck(panel: HTMLElement, sequence: KeyStep[]) {
const user = userEvent.setup()
for (const { key, ctrlKey, expected } of sequence) {
let keyString = ''
if (ctrlKey) keyString += '{Control>}'
keyString += key.length === 1 ? key : `{${key}}`
if (ctrlKey) keyString += '{/Control}'
await user.keyboard(keyString)
// 检查是否只有一个 focused item
const focused = panel.querySelectorAll('.focused')
expect(focused.length).toBe(1)
// 检查 focused item 是否包含预期文本
const text = focused[0].textContent || ''
if (typeof expected === 'string') {
expect(text).toContain(expected)
} else {
expect(expected(text)).toBe(true)
}
}
}
it('should focus on the first item after panel open', () => {
const list = createList(100)
render(
<QuickPanelProvider>
<QuickPanelView setInputText={vi.fn()} />
<OpenPanelOnMount list={list} />
</QuickPanelProvider>
)
// 检查第一个 item 是否有 focused
const item1 = screen.getByText('Item 1')
const focused = item1.closest('.focused')
expect(focused).not.toBeNull()
expect(item1).toBeInTheDocument()
})
it('should focus on the right item using ArrowUp, ArrowDown', async () => {
const list = createList(100, 'Item')
render(
<QuickPanelProvider>
<QuickPanelView setInputText={vi.fn()} />
<OpenPanelOnMount list={list} />
</QuickPanelProvider>
)
const keySequence = [
{ key: 'ArrowUp', expected: 'Item 100' },
{ key: 'ArrowUp', expected: 'Item 99' },
{ key: 'ArrowDown', expected: 'Item 100' },
{ key: 'ArrowDown', expected: 'Item 1' }
]
await runKeySequenceAndCheck(screen.getByTestId('quick-panel'), keySequence)
})
it('should focus on the right item using PageUp, PageDown', async () => {
const list = createList(100, 'Item')
render(
<QuickPanelProvider>
<QuickPanelView setInputText={vi.fn()} />
<OpenPanelOnMount list={list} />
</QuickPanelProvider>
)
const keySequence = [
{ key: 'PageUp', expected: 'Item 1' }, // 停留在顶部
{ key: 'ArrowUp', expected: 'Item 100' },
{ key: 'PageDown', expected: 'Item 100' }, // 停留在底部
{ key: 'PageUp', expected: `Item ${100 - PAGE_SIZE}` },
{ key: 'PageDown', expected: 'Item 100' }
]
await runKeySequenceAndCheck(screen.getByTestId('quick-panel'), keySequence)
})
it('should focus on the right item using Ctrl+ArrowUp, Ctrl+ArrowDown', async () => {
const list = createList(100, 'Item')
render(
<QuickPanelProvider>
<QuickPanelView setInputText={vi.fn()} />
<OpenPanelOnMount list={list} />
</QuickPanelProvider>
)
const keySequence = [
{ key: 'ArrowDown', ctrlKey: true, expected: `Item ${PAGE_SIZE + 1}` },
{ key: 'ArrowUp', ctrlKey: true, expected: 'Item 1' },
{ key: 'ArrowUp', ctrlKey: true, expected: 'Item 100' },
{ key: 'ArrowDown', ctrlKey: true, expected: 'Item 1' }
]
await runKeySequenceAndCheck(screen.getByTestId('quick-panel'), keySequence)
})
})
})

View File

@ -0,0 +1,191 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import Scrollbar from '../Scrollbar'
// Mock lodash throttle
vi.mock('lodash', async () => {
const actual = await import('lodash')
return {
...actual,
throttle: vi.fn((fn) => {
// 简单地直接返回函数,不实际执行节流
const throttled = (...args: any[]) => fn(...args)
throttled.cancel = vi.fn()
return throttled
})
}
})
describe('Scrollbar', () => {
beforeEach(() => {
// 使用 fake timers
vi.useFakeTimers()
})
afterEach(() => {
// 恢复真实的 timers
vi.restoreAllMocks()
vi.useRealTimers()
})
describe('rendering', () => {
it('should render children correctly', () => {
render(
<Scrollbar data-testid="scrollbar">
<div data-testid="child"></div>
</Scrollbar>
)
const child = screen.getByTestId('child')
expect(child).toBeDefined()
expect(child.textContent).toBe('测试内容')
})
it('should pass custom props to container', () => {
render(
<Scrollbar data-testid="scrollbar" className="custom-class">
</Scrollbar>
)
const scrollbar = screen.getByTestId('scrollbar')
expect(scrollbar.className).toContain('custom-class')
})
it('should match default styled snapshot', () => {
const { container } = render(<Scrollbar data-testid="scrollbar"></Scrollbar>)
expect(container.firstChild).toMatchSnapshot()
})
})
describe('scrolling behavior', () => {
it('should update isScrolling state when scrolled', () => {
render(<Scrollbar data-testid="scrollbar"></Scrollbar>)
const scrollbar = screen.getByTestId('scrollbar')
// 初始状态下应该不是滚动状态
expect(scrollbar.getAttribute('isScrolling')).toBeFalsy()
// 触发滚动
fireEvent.scroll(scrollbar)
// 由于 isScrolling 是组件内部状态,不直接反映在 DOM 属性上
// 但可以检查模拟的事件处理是否被调用
expect(scrollbar).toBeDefined()
})
it('should reset isScrolling after timeout', () => {
render(<Scrollbar data-testid="scrollbar"></Scrollbar>)
const scrollbar = screen.getByTestId('scrollbar')
// 触发滚动
fireEvent.scroll(scrollbar)
// 前进时间但不超过timeout
act(() => {
vi.advanceTimersByTime(1000)
})
// 前进超过timeout
act(() => {
vi.advanceTimersByTime(600)
})
// 不测试样式,这里只检查组件是否存在
expect(scrollbar).toBeDefined()
})
it('should reset timeout on continuous scrolling', () => {
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
render(<Scrollbar data-testid="scrollbar"></Scrollbar>)
const scrollbar = screen.getByTestId('scrollbar')
// 第一次滚动
fireEvent.scroll(scrollbar)
// 前进一部分时间
act(() => {
vi.advanceTimersByTime(800)
})
// 再次滚动
fireEvent.scroll(scrollbar)
// clearTimeout 应该被调用,因为在第二次滚动时会清除之前的定时器
expect(clearTimeoutSpy).toHaveBeenCalled()
})
})
describe('throttling', () => {
it('should use throttled scroll handler', async () => {
const { throttle } = await import('lodash')
render(<Scrollbar data-testid="scrollbar"></Scrollbar>)
// 验证 throttle 被调用
expect(throttle).toHaveBeenCalled()
// 验证 throttle 调用时使用了 200ms 延迟
expect(throttle).toHaveBeenCalledWith(expect.any(Function), 200)
})
})
describe('cleanup', () => {
it('should clear timeout and cancel throttle on unmount', async () => {
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
const { unmount } = render(<Scrollbar data-testid="scrollbar"></Scrollbar>)
const scrollbar = screen.getByTestId('scrollbar')
// 触发滚动设置定时器
fireEvent.scroll(scrollbar)
// 卸载组件
unmount()
// 验证 clearTimeout 被调用
expect(clearTimeoutSpy).toHaveBeenCalled()
// 验证 throttle.cancel 被调用
const { throttle } = await import('lodash')
const throttledFunction = (throttle as unknown as Mock).mock.results[0].value
expect(throttledFunction.cancel).toHaveBeenCalled()
})
})
describe('props handling', () => {
it('should handle right prop correctly', () => {
const { container } = render(
<Scrollbar data-testid="scrollbar" right>
</Scrollbar>
)
const scrollbar = screen.getByTestId('scrollbar')
// 验证 right 属性被正确传递
expect(scrollbar).toBeDefined()
// snapshot 测试 styled-components 样式
expect(container.firstChild).toMatchSnapshot()
})
it('should handle ref forwarding', () => {
const ref = { current: null }
render(
<Scrollbar data-testid="scrollbar" ref={ref}>
</Scrollbar>
)
// 验证 ref 被正确设置
expect(ref.current).not.toBeNull()
})
})
})

View File

@ -0,0 +1,74 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`DragableList > snapshot > should match snapshot 1`] = `
<div>
<div
data-testid="drag-drop-context"
>
<div
data-testid="droppable"
>
<div>
<div
data-testid="virtual-list"
>
<div
data-testid="virtual-list-item"
>
<div
data-testid="draggable-a-0"
>
<div
style="margin-bottom: 8px;"
>
<div
data-testid="item"
>
A
</div>
</div>
</div>
</div>
<div
data-testid="virtual-list-item"
>
<div
data-testid="draggable-b-1"
>
<div
style="margin-bottom: 8px;"
>
<div
data-testid="item"
>
B
</div>
</div>
</div>
</div>
<div
data-testid="virtual-list-item"
>
<div
data-testid="draggable-c-2"
>
<div
style="margin-bottom: 8px;"
>
<div
data-testid="item"
>
C
</div>
</div>
</div>
</div>
</div>
<div
data-testid="placeholder"
/>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,45 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Scrollbar > props handling > should handle right prop correctly 1`] = `
.c0 {
overflow-y: auto;
}
.c0::-webkit-scrollbar-thumb {
transition: background 2s ease;
background: transparent;
}
.c0::-webkit-scrollbar-thumb:hover {
background: transparent;
}
<div
class="c0"
data-testid="scrollbar"
>
内容
</div>
`;
exports[`Scrollbar > rendering > should match default styled snapshot 1`] = `
.c0 {
overflow-y: auto;
}
.c0::-webkit-scrollbar-thumb {
transition: background 2s ease;
background: transparent;
}
.c0::-webkit-scrollbar-thumb:hover {
background: transparent;
}
<div
class="c0"
data-testid="scrollbar"
>
内容
</div>
`;

View File

@ -12,7 +12,7 @@ import { checkApi, formatApiKeys } from '@renderer/services/ApiService'
import { checkModelsHealth, getModelCheckSummary } from '@renderer/services/HealthCheckService'
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { formatApiHost, splitApiKeyString } from '@renderer/utils/api'
import { lightbulbVariants } from '@renderer/utils/motionVariants'
import { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link'
@ -127,10 +127,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
return
}
const keys = apiKey
.split(',')
.map((k) => k.trim())
.filter((k) => k)
const keys = splitApiKeyString(apiKey)
// Add an empty key to enable health checks for local models.
// Error messages will be shown for each model if a valid key is needed.
@ -215,11 +212,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
}
if (apiKey.includes(',')) {
const keys = apiKey
.split(/(?<!\\),/)
.map((k) => k.trim())
.map((k) => k.replace(/\\,/g, ','))
.filter((k) => k)
const keys = splitApiKeyString(apiKey)
const result = await ApiCheckPopup.show({
title: t('settings.provider.check_multiple_keys'),

View File

@ -1,124 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`markdown > markdown configuration constants > sanitizeSchema matches snapshot 1`] = `
{
"attributes": {
"*": [
"className",
"style",
"id",
"title",
],
"a": [
"href",
"target",
"rel",
],
"circle": [
"cx",
"cy",
"r",
"fill",
"stroke",
],
"g": [
"transform",
"fill",
"stroke",
],
"line": [
"x1",
"y1",
"x2",
"y2",
"stroke",
],
"path": [
"d",
"fill",
"stroke",
"strokeWidth",
"strokeLinecap",
"strokeLinejoin",
],
"polygon": [
"points",
"fill",
"stroke",
],
"polyline": [
"points",
"fill",
"stroke",
],
"rect": [
"x",
"y",
"width",
"height",
"fill",
"stroke",
],
"svg": [
"viewBox",
"width",
"height",
"xmlns",
"fill",
"stroke",
],
"text": [
"x",
"y",
"fill",
"textAnchor",
"dominantBaseline",
],
},
"tagNames": [
"style",
"p",
"div",
"span",
"b",
"i",
"strong",
"em",
"ul",
"ol",
"li",
"table",
"tr",
"td",
"th",
"thead",
"tbody",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"pre",
"code",
"br",
"hr",
"svg",
"path",
"circle",
"rect",
"line",
"polyline",
"polygon",
"text",
"g",
"defs",
"title",
"desc",
"tspan",
"sub",
"sup",
],
}
`;

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { formatApiHost, maskApiKey } from '../api'
import { formatApiHost, maskApiKey, splitApiKeyString } from '../api'
describe('api', () => {
describe('formatApiHost', () => {
@ -67,4 +67,60 @@ describe('api', () => {
expect(maskApiKey('12345678')).toBe('12345678')
})
})
describe('splitApiKeyString', () => {
it('should split comma-separated keys', () => {
const input = 'key1,key2,key3'
const result = splitApiKeyString(input)
expect(result).toEqual(['key1', 'key2', 'key3'])
})
it('should trim spaces around keys', () => {
const input = ' key1 , key2 ,key3 '
const result = splitApiKeyString(input)
expect(result).toEqual(['key1', 'key2', 'key3'])
})
it('should handle escaped commas', () => {
const input = 'key1,key2\\,withcomma,key3'
const result = splitApiKeyString(input)
expect(result).toEqual(['key1', 'key2,withcomma', 'key3'])
})
it('should handle multiple escaped commas', () => {
const input = 'key1\\,withcomma1,key2\\,withcomma2'
const result = splitApiKeyString(input)
expect(result).toEqual(['key1,withcomma1', 'key2,withcomma2'])
})
it('should ignore empty keys', () => {
const input = 'key1,,key2, ,key3'
const result = splitApiKeyString(input)
expect(result).toEqual(['key1', 'key2', 'key3'])
})
it('should return empty array for empty string', () => {
const input = ''
const result = splitApiKeyString(input)
expect(result).toEqual([])
})
it('should handle only one key', () => {
const input = 'singlekey'
const result = splitApiKeyString(input)
expect(result).toEqual(['singlekey'])
})
it('should handle only escaped comma', () => {
const input = 'key\\,withcomma'
const result = splitApiKeyString(input)
expect(result).toEqual(['key,withcomma'])
})
it('should handle all keys with spaces and escaped commas', () => {
const input = ' key1 , key2\\,withcomma , key3 '
const result = splitApiKeyString(input)
expect(result).toEqual(['key1', 'key2,withcomma', 'key3'])
})
})
})

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { delay, runAsyncFunction } from '../index'
import { runAsyncFunction } from '../index'
import { compareVersions, hasPath, isFreeModel, isValidProxyUrl, removeQuotes, removeSpecialCharacters } from '../index'
describe('Unclassified Utils', () => {
describe('runAsyncFunction', () => {
@ -23,24 +24,118 @@ describe('Unclassified Utils', () => {
})
})
describe('delay', () => {
it('should resolve after specified seconds', async () => {
// 验证指定时间后返回
const start = Date.now()
await delay(0.01)
const end = Date.now()
// In JavaScript, the delay time of setTimeout is not always precise
// and may be slightly shorter than specified. Make it more lenient:
const lenientRatio = 0.8
expect(end - start).toBeGreaterThanOrEqual(10 * lenientRatio)
describe('isFreeModel', () => {
const base = { provider: '', group: '' }
it('should return true if id or name contains "free" (case-insensitive)', () => {
expect(isFreeModel({ id: 'free-model', name: 'test', ...base })).toBe(true)
expect(isFreeModel({ id: 'model', name: 'FreePlan', ...base })).toBe(true)
expect(isFreeModel({ id: 'model', name: 'notfree', ...base })).toBe(true)
expect(isFreeModel({ id: 'model', name: 'test', ...base })).toBe(false)
})
it('should resolve immediately for zero delay', async () => {
// 验证零延迟立即返回
const start = Date.now()
await delay(0)
const end = Date.now()
expect(end - start).toBeLessThan(100)
it('should handle empty id or name', () => {
expect(isFreeModel({ id: '', name: 'free', ...base })).toBe(true)
expect(isFreeModel({ id: 'free', name: '', ...base })).toBe(true)
expect(isFreeModel({ id: '', name: '', ...base })).toBe(false)
})
})
describe('removeQuotes', () => {
it('should remove all single and double quotes', () => {
expect(removeQuotes('"hello"')).toBe('hello')
expect(removeQuotes("'hello'")).toBe('hello')
expect(removeQuotes('"hello"')).toBe('hello')
expect(removeQuotes('noquotes')).toBe('noquotes')
})
it('should handle empty string', () => {
expect(removeQuotes('')).toBe('')
})
it('should handle string with only quotes', () => {
expect(removeQuotes('""')).toBe('')
expect(removeQuotes("''")).toBe('')
})
})
describe('removeSpecialCharacters', () => {
it('should remove newlines, quotes, and special characters', () => {
expect(removeSpecialCharacters('hello\nworld!')).toBe('helloworld')
expect(removeSpecialCharacters('"hello, world!"')).toBe('hello world')
expect(removeSpecialCharacters('你好,世界!')).toBe('你好世界')
})
it('should handle empty string', () => {
expect(removeSpecialCharacters('')).toBe('')
})
it('should handle string with only special characters', () => {
expect(removeSpecialCharacters('"\n!,.')).toBe('')
})
})
describe('isValidProxyUrl', () => {
it('should return true for string containing "://"', () => {
expect(isValidProxyUrl('http://localhost')).toBe(true)
expect(isValidProxyUrl('socks5://127.0.0.1:1080')).toBe(true)
})
it('should return false for string not containing "://"', () => {
expect(isValidProxyUrl('localhost')).toBe(false)
expect(isValidProxyUrl('127.0.0.1:1080')).toBe(false)
})
it('should handle empty string', () => {
expect(isValidProxyUrl('')).toBe(false)
})
it('should return true for only "://"', () => {
expect(isValidProxyUrl('://')).toBe(true)
})
})
describe('hasPath', () => {
it('should return true if url has path', () => {
expect(hasPath('http://a.com/path')).toBe(true)
expect(hasPath('http://a.com/path/to')).toBe(true)
})
it('should return false if url has no path or only root', () => {
expect(hasPath('http://a.com/')).toBe(false)
expect(hasPath('http://a.com')).toBe(false)
})
it('should return false for invalid url', () => {
expect(hasPath('not a url')).toBe(false)
expect(hasPath('')).toBe(false)
})
})
describe('compareVersions', () => {
it('should return 1 if v1 > v2', () => {
expect(compareVersions('1.2.3', '1.2.2')).toBe(1)
expect(compareVersions('2.0.0', '1.9.9')).toBe(1)
expect(compareVersions('1.2.0', '1.1.9')).toBe(1)
expect(compareVersions('1.2.3', '1.2')).toBe(1)
})
it('should return -1 if v1 < v2', () => {
expect(compareVersions('1.2.2', '1.2.3')).toBe(-1)
expect(compareVersions('1.9.9', '2.0.0')).toBe(-1)
expect(compareVersions('1.1.9', '1.2.0')).toBe(-1)
expect(compareVersions('1.2', '1.2.3')).toBe(-1)
})
it('should return 0 if v1 == v2', () => {
expect(compareVersions('1.2.3', '1.2.3')).toBe(0)
expect(compareVersions('1.2', '1.2.0')).toBe(0)
expect(compareVersions('1.0.0', '1')).toBe(0)
})
it('should handle non-numeric and empty string', () => {
expect(compareVersions('', '')).toBe(0)
expect(compareVersions('a.b.c', '1.2.3')).toBe(-1)
expect(compareVersions('1.2.3', 'a.b.c')).toBe(1)
})
})
})

View File

@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'
import {
convertMathFormula,
encodeHTML,
findCitationInChildren,
getCodeBlockId,
removeTrailingDoubleSpaces,
@ -14,8 +15,8 @@ import {
describe('markdown', () => {
describe('findCitationInChildren', () => {
it('returns null when children is null or undefined', () => {
expect(findCitationInChildren(null)).toBeNull()
expect(findCitationInChildren(undefined)).toBeNull()
expect(findCitationInChildren(null)).toBe('')
expect(findCitationInChildren(undefined)).toBe('')
})
it('finds citation in direct child element', () => {
@ -36,7 +37,7 @@ describe('markdown', () => {
it('returns null when no citation is found', () => {
const children = [{ props: { foo: 'bar' } }, { props: { children: [{ props: { baz: 'qux' } }] } }]
expect(findCitationInChildren(children)).toBeNull()
expect(findCitationInChildren(children)).toBe('')
})
it('handles single child object (non-array)', () => {
@ -107,6 +108,7 @@ describe('markdown', () => {
it('should return input if null or empty', () => {
// 验证空输入或 null 输入时返回原值
expect(convertMathFormula('')).toBe('')
// @ts-expect-error purposely pass wrong type to test error branch
expect(convertMathFormula(null)).toBe(null)
})
})
@ -141,6 +143,41 @@ describe('markdown', () => {
})
})
describe('encodeHTML', () => {
it('should encode all special HTML characters', () => {
const input = `Tom & Jerry's "cat" <dog>`
const result = encodeHTML(input)
expect(result).toBe('Tom &amp; Jerry&apos;s &quot;cat&quot; &lt;dog&gt;')
})
it('should return the same string if no special characters', () => {
const input = 'Hello World!'
const result = encodeHTML(input)
expect(result).toBe('Hello World!')
})
it('should return empty string if input is empty', () => {
const input = ''
const result = encodeHTML(input)
expect(result).toBe('')
})
it('should encode single special character', () => {
expect(encodeHTML('&')).toBe('&amp;')
expect(encodeHTML('<')).toBe('&lt;')
expect(encodeHTML('>')).toBe('&gt;')
expect(encodeHTML('"')).toBe('&quot;')
expect(encodeHTML("'")).toBe('&apos;')
})
it('should throw if input is not a string', () => {
// @ts-expect-error purposely pass wrong type to test error branch
expect(() => encodeHTML(null)).toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
expect(() => encodeHTML(undefined)).toThrow()
})
})
describe('getCodeBlockId', () => {
it('should generate ID from position information', () => {
// 从位置信息生成ID

View File

@ -1,22 +1,14 @@
import { MCPTool } from '@renderer/types'
import { type MCPTool } from '@renderer/types'
import { describe, expect, it } from 'vitest'
import { AvailableTools, buildSystemPrompt } from '../prompt'
describe('prompt', () => {
// 辅助函数:创建符合 MCPTool 类型的工具对象
const createMcpTool = (id: string, description: string, inputSchema: any): MCPTool => ({
id,
description,
inputSchema,
serverId: 'test-server-id',
serverName: 'test-server',
name: id
})
describe('AvailableTools', () => {
it('should generate XML format for tools', () => {
const tools = [createMcpTool('test-tool', 'Test tool description', { type: 'object' })]
const tools = [
{ id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool
]
const result = AvailableTools(tools)
expect(result).toContain('<tools>')
@ -39,7 +31,9 @@ describe('prompt', () => {
describe('buildSystemPrompt', () => {
it('should build prompt with tools', () => {
const userPrompt = 'Custom user system prompt'
const tools = [createMcpTool('test-tool', 'Test tool description', { type: 'object' })]
const tools = [
{ id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool
]
const result = buildSystemPrompt(userPrompt, tools)
expect(result).toContain(userPrompt)
@ -55,7 +49,9 @@ describe('prompt', () => {
})
it('should handle null or undefined user prompt', () => {
const tools = [createMcpTool('test-tool', 'Test tool description', { type: 'object' })]
const tools = [
{ id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool
]
// 测试 userPrompt 为 null 的情况
const resultNull = buildSystemPrompt(null as any, tools)

View File

@ -1,4 +1,14 @@
export function formatApiHost(host: string) {
/**
* API
*
* host `/v1/`
* - host `/` `volces.com/api/v3`
* -
*
* @param {string} host - API
* @returns {string} API
*/
export function formatApiHost(host: string): string {
const forceUseOriginalHost = () => {
if (host.endsWith('/')) {
return true
@ -10,6 +20,17 @@ export function formatApiHost(host: string) {
return forceUseOriginalHost() ? host : `${host}/v1/`
}
/**
* API key
*
* - 24 8
* - 16 4
* - 8 2
* -
*
* @param {string} key - API
* @returns {string}
*/
export function maskApiKey(key: string): string {
if (!key) return ''
@ -23,3 +44,17 @@ export function maskApiKey(key: string): string {
return key
}
}
/**
* API key key
*
* @param {string} keyStr - API key
* @returns {string[]} API key
*/
export function splitApiKeyString(keyStr: string): string[] {
return keyStr
.split(/(?<!\\),/)
.map((k) => k.trim())
.map((k) => k.replace(/\\,/g, ','))
.filter((k) => k)
}

View File

@ -15,9 +15,9 @@ import dayjs from 'dayjs'
/**
*
* @param str
* @param length 80
* @returns string
* @param {string} str
* @param {number} [length=80] 80
* @returns {string}
*/
export function getTitleFromString(str: string, length: number = 80) {
let title = str.trimStart().split('\n')[0]

View File

@ -17,8 +17,8 @@ export interface KnowledgeExtractResults {
/**
* XML标签的文本中提取信息
* @public
* @param text XML标签的文本
* @returns
* @param {string} text XML标签的文本
* @returns {ExtractResults}
* @throws
*/
export const extractInfoFromXML = (text: string): ExtractResults => {

View File

@ -2,10 +2,10 @@ import { KB, MB } from '@shared/config/constant'
/**
*
* @param filePath
* @returns string
* @param {string} filePath
* @returns {string}
*/
export function getFileDirectory(filePath: string) {
export function getFileDirectory(filePath: string): string {
const parts = filePath.split('/')
const directory = parts.slice(0, -1).join('/')
return directory
@ -13,10 +13,10 @@ export function getFileDirectory(filePath: string) {
/**
*
* @param filePath
* @returns string '.'
* @param {string} filePath
* @returns {string} '.'
*/
export function getFileExtension(filePath: string) {
export function getFileExtension(filePath: string): string {
const parts = filePath.split('.')
if (parts.length > 1) {
const extension = parts.slice(-1)[0].toLowerCase()
@ -27,10 +27,10 @@ export function getFileExtension(filePath: string) {
/**
* MB KB
* @param size
* @returns string
* @param {number} size
* @returns {string}
*/
export function formatFileSize(size: number) {
export function formatFileSize(size: number): string {
if (size >= MB) {
return (size / MB).toFixed(1) + ' MB'
}
@ -46,10 +46,10 @@ export function formatFileSize(size: number) {
*
* - 线
* -
* @param str
* @returns string
* @param {string} str
* @returns {string}
*/
export function removeSpecialCharactersForFileName(str: string) {
export function removeSpecialCharactersForFileName(str: string): string {
return str
.replace(/[<>:"/\\|?*.]/g, '_')
.replace(/[\r\n]+/g, ' ')

View File

@ -4,8 +4,8 @@ import * as htmlToImage from 'html-to-image'
/**
* Base64 ArrayBuffer
* @param file
* @returns Promise<string | ArrayBuffer | null> Base64 null
* @param {File} file
* @returns {Promise<string | ArrayBuffer | null>} Base64 null
*/
export const convertToBase64 = (file: File): Promise<string | ArrayBuffer | null> => {
return new Promise((resolve, reject) => {
@ -18,10 +18,10 @@ export const convertToBase64 = (file: File): Promise<string | ArrayBuffer | null
/**
*
* @param file
* @returns Promise<File>
* @param {File} file
* @returns {Promise<File>}
*/
export const compressImage = async (file: File) => {
export const compressImage = async (file: File): Promise<File> => {
return await imageCompression(file, {
maxSizeMB: 1,
maxWidthOrHeight: 300,

View File

@ -6,19 +6,19 @@ import { v4 as uuidv4 } from 'uuid'
/**
*
* @param fn
* @returns Promise<void>
* @param {() => void} fn
* @returns {Promise<void>}
*/
export const runAsyncFunction = async (fn: () => void) => {
export const runAsyncFunction = async (fn: () => void): Promise<void> => {
await fn()
}
/**
* Promise
* @param seconds
* @returns Promise<any> Promise
* @param {number} seconds
* @returns {Promise<any>} Promise
*/
export const delay = (seconds: number) => {
export const delay = (seconds: number): Promise<any> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
@ -27,9 +27,17 @@ export const delay = (seconds: number) => {
}
/**
* Waiting fn return true
**/
export const waitAsyncFunction = (fn: () => Promise<any>, interval = 200, stopTimeout = 60000) => {
* true
* @param {() => Promise<any>} fn
* @param {number} [interval=200]
* @param {number} [stopTimeout=60000]
* @returns {Promise<any>} true Promise
*/
export const waitAsyncFunction = (
fn: () => Promise<any>,
interval: number = 200,
stopTimeout: number = 60000
): Promise<any> => {
let timeout = false
const timer = setTimeout(() => (timeout = true), stopTimeout)
@ -63,10 +71,10 @@ export async function isDev() {
/**
*
* @param error
* @returns string
* @param {any} error
* @returns {string}
*/
export function getErrorMessage(error: any) {
export function getErrorMessage(error: any): string {
if (!error) {
return ''
}
@ -86,21 +94,31 @@ export function getErrorMessage(error: any) {
return ''
}
export function removeQuotes(str) {
/**
*
* @param {string} str
* @returns {string}
*/
export function removeQuotes(str: string): string {
return str.replace(/['"]+/g, '')
}
export function removeSpecialCharacters(str: string) {
/**
*
* @param {string} str
* @returns {string}
*/
export function removeSpecialCharacters(str: string): string {
// First remove newlines and quotes, then remove other special characters
return str.replace(/[\n"]/g, '').replace(/[\p{M}\p{P}]/gu, '')
}
/**
* is valid proxy url
* @param url proxy url
* @returns boolean
* URL URL
* @param {string} url URL
* @returns {boolean}
*/
export const isValidProxyUrl = (url: string) => {
export const isValidProxyUrl = (url: string): boolean => {
return url.includes('://')
}
@ -124,8 +142,8 @@ export function loadScript(url: string) {
/**
* URL
* @param url URL
* @returns boolean URL true false
* @param {string} url URL
* @returns {boolean} URL true false
*/
export function hasPath(url: string): boolean {
try {
@ -139,9 +157,9 @@ export function hasPath(url: string): boolean {
/**
*
* @param v1
* @param v2
* @returns number 1 v1 v2-1 v1 v20
* @param {string} v1
* @param {string} v2
* @returns {number} 1 v1 v2-1 v1 v20
*/
export const compareVersions = (v1: string, v2: string): number => {
const v1Parts = v1.split('.').map(Number)
@ -158,10 +176,10 @@ export const compareVersions = (v1: string, v2: string): number => {
/**
*
* @param params
* @returns Promise<boolean> true false
* @param {ModalFuncProps} params
* @returns {Promise<boolean>} true false
*/
export function modalConfirm(params: ModalFuncProps) {
export function modalConfirm(params: ModalFuncProps): Promise<boolean> {
return new Promise((resolve) => {
window.modal.confirm({
centered: true,
@ -174,11 +192,11 @@ export function modalConfirm(params: ModalFuncProps) {
/**
*
* @param obj
* @param key
* @returns boolean true false
* @param {any} obj
* @param {string} key
* @returns {boolean} true false
*/
export function hasObjectKey(obj: any, key: string) {
export function hasObjectKey(obj: any, key: string): boolean {
if (typeof obj !== 'object' || obj === null) {
return false
}
@ -188,10 +206,10 @@ export function hasObjectKey(obj: any, key: string) {
/**
* npm readme中提取 npx mcp config
* @param readme readme字符串
* @returns mcp config sample
* @param {string} readme readme字符串
* @returns {Record<string, any> | null} mcp config sample
*/
export function getMcpConfigSampleFromReadme(readme: string) {
export function getMcpConfigSampleFromReadme(readme: string): Record<string, any> | null {
if (readme) {
try {
const regex = /"mcpServers"\s*:\s*({(?:[^{}]*|{(?:[^{}]*|{[^{}]*})*})*})/g

View File

@ -1,6 +1,7 @@
/**
* json
* @param str
* @param {any} str
* @returns {boolean} json
*/
export function isJSON(str: any): boolean {
if (typeof str !== 'string') {
@ -16,10 +17,10 @@ export function isJSON(str: any): boolean {
/**
* JSON null
* @param str
* @returns null
* @param {string} str
* @returns {any | null} null
*/
export function parseJSON(str: string) {
export function parseJSON(str: string): any | null {
try {
return JSON.parse(str)
} catch (e) {

View File

@ -7,8 +7,8 @@ let urlToCounterMap: Map<string, number> = new Map()
/**
* Determines if a string looks like a host/URL
* @param text The text to check
* @returns Boolean indicating if the text is likely a host
* @param {string} text The text to check
* @returns {boolean} Boolean indicating if the text is likely a host
*/
function isHost(text: string): boolean {
// Basic check for URL-like patterns
@ -18,11 +18,11 @@ function isHost(text: string): boolean {
/**
* Converts Markdown links in the text to numbered links based on the rules:s
* [ref_N] -> [<sup>N</sup>]
* @param text The current chunk of text to process
* @param resetCounter Whether to reset the counter and buffer
* @returns Processed text with complete links converted
* @param {string} text The current chunk of text to process
* @param {boolean} resetCounter Whether to reset the counter and buffer
* @returns {string} Processed text with complete links converted
*/
export function convertLinksToZhipu(text: string, resetCounter = false): string {
export function convertLinksToZhipu(text: string, resetCounter: boolean = false): string {
if (resetCounter) {
linkCounter = 1
buffer = ''
@ -57,7 +57,16 @@ export function convertLinksToZhipu(text: string, resetCounter = false): string
})
}
export function convertLinksToHunyuan(text: string, webSearch: any[], resetCounter = false): string {
/**
* Converts Markdown links in the text to numbered links based on the rules:
* [N](@ref) -> [<sup>N</sup>]()
* [N,M,...](@ref) -> [<sup>N</sup>]() [<sup>M</sup>]() ...
* @param {string} text The current chunk of text to process
* @param {any[]} webSearch webSearch results
* @param {boolean} resetCounter Whether to reset the counter and buffer
* @returns {string} Processed text with complete links converted
*/
export function convertLinksToHunyuan(text: string, webSearch: any[], resetCounter: boolean = false): string {
if (resetCounter) {
linkCounter = 1
buffer = ''
@ -115,11 +124,11 @@ export function convertLinksToHunyuan(text: string, webSearch: any[], resetCount
* 2. [host](url) -> [cnt](url)
* 3. [any text except url](url)-> any text [cnt](url)
*
* @param text The current chunk of text to process
* @param resetCounter Whether to reset the counter and buffer
* @returns Processed text with complete links converted
* @param {string} text The current chunk of text to process
* @param {boolean} resetCounter Whether to reset the counter and buffer
* @returns {string} Processed text with complete links converted
*/
export function convertLinks(text: string, resetCounter = false): string {
export function convertLinks(text: string, resetCounter: boolean = false): string {
if (resetCounter) {
linkCounter = 1
buffer = ''
@ -235,9 +244,9 @@ export function convertLinks(text: string, resetCounter = false): string {
* Converts Markdown links in the text to numbered links based on the rules:
* 1. [host](url) -> [cnt](url)
*
* @param text The current chunk of text to process
* @param resetCounter Whether to reset the counter and buffer
* @returns Processed text with complete links converted
* @param {string} text The current chunk of text to process
* @param {boolean} resetCounter Whether to reset the counter and buffer
* @returns {string} Processed text with complete links converted
*/
export function convertLinksToOpenRouter(text: string, resetCounter = false): string {
if (resetCounter) {
@ -292,9 +301,9 @@ export function convertLinksToOpenRouter(text: string, resetCounter = false): st
/**
* webSearch结果补全链接[<sup>num</sup>]()[<sup>num</sup>](webSearch[num-1].url)
* @param text
* @param webSearch webSearch结果
* @returns
* @param {string} text
* @param {any[]} webSearch webSearch结果
* @returns {string}
*/
export function completeLinks(text: string, webSearch: any[]): string {
// 使用正则表达式匹配形如 [<sup>num</sup>]() 的链接
@ -316,8 +325,8 @@ export function completeLinks(text: string, webSearch: any[]): string {
* 2. [<sup>num</sup>](url)
* 3. ([text](url))
*
* @param text Markdown格式的文本
* @returns URL数组
* @param {string} text Markdown格式的文本
* @returns {string[]} URL数组
*/
export function extractUrlsFromMarkdown(text: string): string[] {
const urlSet = new Set<string>()
@ -338,8 +347,8 @@ export function extractUrlsFromMarkdown(text: string): string[] {
/**
* URL
* @param url URL字符串
* @returns URL
* @param {string} url URL字符串
* @returns {boolean} URL
*/
function isValidUrl(url: string): boolean {
try {
@ -353,8 +362,8 @@ function isValidUrl(url: string): boolean {
/**
* Markdown
* : [text](url),[text](url) -> [text](url) [text](url)
* @param text Markdown
* @returns
* @param {string} text Markdown
* @returns {string}
*/
export function cleanLinkCommas(text: string): string {
// 匹配两个 Markdown 链接之间的英文逗号(可能包含空格)

View File

@ -3,9 +3,13 @@ import remarkStringify from 'remark-stringify'
import { unified } from 'unified'
import { visit } from 'unist-util-visit'
// 更彻底的查找方法,递归搜索所有子元素
export const findCitationInChildren = (children) => {
if (!children) return null
/**
*
* @param {any} children
* @returns {string} citation ''
*/
export const findCitationInChildren = (children: any): string => {
if (!children) return ''
// 直接搜索子元素
for (const child of Array.isArray(children) ? children : [children]) {
@ -20,17 +24,17 @@ export const findCitationInChildren = (children) => {
}
}
return null
return ''
}
/**
*
* - LaTeX '\\[' '\\]' '$$$$'
* - LaTeX '\\(' '\\)' '$$'
* @param input
* @returns string
* @param {string} input
* @returns {string}
*/
export function convertMathFormula(input) {
export function convertMathFormula(input: string): string {
if (!input) return input
let result = input
@ -41,8 +45,8 @@ export function convertMathFormula(input) {
/**
* Markdown
* @param markdown Markdown
* @returns string
* @param {string} markdown Markdown
* @returns {string}
*/
export function removeTrailingDoubleSpaces(markdown: string): string {
// 使用正则表达式匹配末尾的两个空格,并替换为空字符串

View File

@ -12,11 +12,11 @@
* - 'deepseek-r1' => 'deepseek-r1'
* - 'o3' => 'o3'
*
* @param id ID
* @param provider ID
* @returns string
* @param {string} id ID
* @param {string} [provider] ID
* @returns {string}
*/
export const getDefaultGroupName = (id: string, provider?: string) => {
export const getDefaultGroupName = (id: string, provider?: string): string => {
const str = id.toLowerCase()
// 定义分隔符
@ -48,8 +48,8 @@ export const getDefaultGroupName = (id: string, provider?: string) => {
/**
* avatar
* @param str
* @returns string
* @param {string} str
* @returns {string}
*/
export function firstLetter(str: string): string {
const match = str?.match(/\p{L}\p{M}*|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u)
@ -58,8 +58,8 @@ export function firstLetter(str: string): string {
/**
*
* @param str
* @returns string
* @param {string} str
* @returns {string}
*/
export function removeLeadingEmoji(str: string): string {
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
@ -68,8 +68,8 @@ export function removeLeadingEmoji(str: string): string {
/**
*
* @param str
* @returns string
* @param {string} str
* @returns {string}
*/
export function getLeadingEmoji(str: string): string {
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
@ -79,8 +79,8 @@ export function getLeadingEmoji(str: string): string {
/**
*
* @param str
* @returns boolean true false
* @param {string} str
* @returns {boolean} true false
*/
export function isEmoji(str: string): boolean {
if (str.startsWith('data:')) {
@ -97,19 +97,19 @@ export function isEmoji(str: string): boolean {
/**
*
* -
* @param str
* @returns string
* @param {string} str
* @returns {string}
*/
export function removeSpecialCharactersForTopicName(str: string) {
export function removeSpecialCharactersForTopicName(str: string): string {
return str.replace(/[\r\n]+/g, ' ').trim()
}
/**
* avatar
* @param char
* @returns string
* @param {string} char
* @returns {string}
*/
export function generateColorFromChar(char: string) {
export function generateColorFromChar(char: string): string {
// 使用字符的Unicode值作为随机种子
const seed = char.charCodeAt(0)
@ -134,23 +134,23 @@ export function generateColorFromChar(char: string) {
/**
*
* @param str
* @returns string
* @param {string} str
* @returns {string}
*/
export function getFirstCharacter(str) {
if (str.length === 0) return ''
export function getFirstCharacter(str: string): string {
// 使用 for...of 循环来获取第一个字符
for (const char of str) {
return char
}
return ''
}
/**
*
* @param text
* @param maxLength 50
* @returns string
* @param {string} text
* @param {number} [maxLength=50] 50
* @returns {string}
*/
export function getBriefInfo(text: string, maxLength: number = 50): string {
// 去除空行

View File

@ -1,13 +1,13 @@
/**
* dnd "拖动"
* @template T
* @param list
* @param sourceIndex
* @param destIndex
* @param len 1
* @returns T[]
* @template {T}
* @param {T[]} list
* @param {number} sourceIndex
* @param {number} destIndex
* @param {number} [len=1] 1
* @returns {T[]}
*/
export function droppableReorder<T>(list: T[], sourceIndex: number, destIndex: number, len = 1) {
export function droppableReorder<T>(list: T[], sourceIndex: number, destIndex: number, len: number = 1): T[] {
const result = Array.from(list)
const removed = result.splice(sourceIndex, len)
@ -21,11 +21,11 @@ export function droppableReorder<T>(list: T[], sourceIndex: number, destIndex: n
/**
*
* @param a
* @param b
* @returns
* @param {string} a
* @param {string} b
* @returns {number}
*/
export function sortByEnglishFirst(a: string, b: string) {
export function sortByEnglishFirst(a: string, b: string): number {
const isAEnglish = /^[a-zA-Z]/.test(a)
const isBEnglish = /^[a-zA-Z]/.test(b)
if (isAEnglish && !isBEnglish) return -1

View File

@ -6,18 +6,18 @@ interface ClassDictionary {
interface ClassArray extends Array<ClassValue> {}
// Example:
// classNames('foo', 'bar'); // => 'foo bar'
// classNames('foo', { bar: true }); // => 'foo bar'
// classNames({ foo: true, bar: false }); // => 'foo'
// classNames(['foo', 'bar']); // => 'foo bar'
// classNames('foo', null, 'bar'); // => 'foo bar'
// classNames({ message: true, 'message-assistant': true }); // => 'message message-assistant'
/**
* class
* @param args
* @returns
*
* Examples:
* classNames('foo', 'bar'); // => 'foo bar'
* classNames('foo', { bar: true }); // => 'foo bar'
* classNames({ foo: true, bar: false }); // => 'foo'
* classNames(['foo', 'bar']); // => 'foo bar'
* classNames('foo', null, 'bar'); // => 'foo bar'
* classNames({ message: true, 'message-assistant': true }); // => 'message message-assistant'
* @param {ClassValue[]} args
* @returns {string}
*/
export function classNames(...args: ClassValue[]): string {
const classes: string[] = []

13
tests/e2e/launch.test.tsx Normal file
View File

@ -0,0 +1,13 @@
import { _electron as electron, expect, test } from '@playwright/test'
let electronApp: any
let window: any
test.describe('App Launch', () => {
test('should launch and close the main application', async () => {
electronApp = await electron.launch({ args: ['.'] })
window = await electronApp.firstWindow()
expect(window).toBeDefined()
await electronApp.close()
})
})

46
tests/renderer.setup.ts Normal file
View File

@ -0,0 +1,46 @@
import '@testing-library/jest-dom/vitest'
import { styleSheetSerializer } from 'jest-styled-components/serializer'
import { expect, vi } from 'vitest'
expect.addSnapshotSerializer(styleSheetSerializer)
vi.mock('electron-log/renderer', () => {
return {
default: {
info: console.log,
error: console.error,
warn: console.warn,
debug: console.debug,
verbose: console.log,
silly: console.log,
log: console.log,
transports: {
console: {
level: 'info'
}
}
}
}
})
vi.mock('axios', () => ({
default: {
get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request
post: vi.fn().mockResolvedValue({ data: {} }) // Mocking axios POST request
// You can add other axios methods like put, delete etc. as needed
}
}))
vi.stubGlobal('electron', {
ipcRenderer: {
on: vi.fn(),
send: vi.fn()
}
})
vi.stubGlobal('api', {
file: {
read: vi.fn().mockResolvedValue('[]'),
writeWithId: vi.fn().mockResolvedValue(undefined)
}
})

View File

@ -2,35 +2,54 @@ import { defineConfig } from 'vitest/config'
import electronViteConfig from './electron.vite.config'
const rendererConfig = electronViteConfig.renderer
const mainConfig = (electronViteConfig as any).main
const rendererConfig = (electronViteConfig as any).renderer
export default defineConfig({
// 复用 renderer 插件和路径别名
// @ts-ignore plugins 类型
plugins: rendererConfig?.plugins,
resolve: {
// @ts-ignore alias 类型
alias: rendererConfig?.resolve.alias
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['@vitest/web-worker', './src/renderer/__tests__/setup.ts'],
include: [
// 只测试渲染进程
'src/renderer/**/*.{test,spec}.{ts,tsx}',
'src/renderer/**/__tests__/**/*.{test,spec}.{ts,tsx}'
workspace: [
// 主进程单元测试配置
{
extends: true,
plugins: mainConfig.plugins,
resolve: {
alias: mainConfig.resolve.alias
},
test: {
name: 'main',
environment: 'node',
include: ['src/main/**/*.{test,spec}.{ts,tsx}', 'src/main/**/__tests__/**/*.{test,spec}.{ts,tsx}']
}
},
// 渲染进程单元测试配置
{
extends: true,
plugins: rendererConfig.plugins,
resolve: {
alias: rendererConfig.resolve.alias
},
test: {
name: 'renderer',
environment: 'jsdom',
setupFiles: ['@vitest/web-worker', 'tests/renderer.setup.ts'],
include: ['src/renderer/**/*.{test,spec}.{ts,tsx}', 'src/renderer/**/__tests__/**/*.{test,spec}.{ts,tsx}']
}
}
],
exclude: ['**/node_modules/**', '**/dist/**', '**/out/**', '**/build/**', '**/src/renderer/__tests__/setup.ts'],
// 全局共享配置
globals: true,
setupFiles: [],
exclude: ['**/node_modules/**', '**/dist/**', '**/out/**', '**/build/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reporter: ['text', 'json', 'html', 'lcov', 'text-summary'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/out/**',
'**/build/**',
'**/coverage/**',
'**/tests/**',
'**/.yarn/**',
'**/.cursor/**',
'**/.vscode/**',
@ -40,9 +59,7 @@ export default defineConfig({
'**/types/**',
'**/__tests__/**',
'**/*.{test,spec}.{ts,tsx}',
'**/*.config.{js,ts}',
'**/electron.vite.config.ts',
'**/vitest.config.ts'
'**/*.config.{js,ts}'
]
},
testTimeout: 20000,

616
yarn.lock
View File

@ -12,6 +12,13 @@ __metadata:
languageName: node
linkType: hard
"@adobe/css-tools@npm:^4.0.1, @adobe/css-tools@npm:^4.4.0":
version: 4.4.2
resolution: "@adobe/css-tools@npm:4.4.2"
checksum: 10c0/19433666ad18536b0ed05d4b53fbb3dd6ede266996796462023ec77a90b484890ad28a3e528cdf3ab8a65cb2fcdff5d8feb04db6bc6eed6ca307c40974239c94
languageName: node
linkType: hard
"@agentic/core@npm:7.6.4":
version: 7.6.4
resolution: "@agentic/core@npm:7.6.4"
@ -67,7 +74,7 @@ __metadata:
languageName: node
linkType: hard
"@ampproject/remapping@npm:^2.2.0":
"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0":
version: 2.3.0
resolution: "@ampproject/remapping@npm:2.3.0"
dependencies:
@ -221,6 +228,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.10.4":
version: 7.27.1
resolution: "@babel/code-frame@npm:7.27.1"
dependencies:
"@babel/helper-validator-identifier": "npm:^7.27.1"
js-tokens: "npm:^4.0.0"
picocolors: "npm:^1.1.1"
checksum: 10c0/5dd9a18baa5fce4741ba729acc3a3272c49c25cb8736c4b18e113099520e7ef7b545a4096a26d600e4416157e63e87d66db46aa3fbf0a5f2286da2705c12da00
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.26.2":
version: 7.26.2
resolution: "@babel/code-frame@npm:7.26.2"
@ -325,6 +343,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-string-parser@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-string-parser@npm:7.27.1"
checksum: 10c0/8bda3448e07b5583727c103560bcf9c4c24b3c1051a4c516d4050ef69df37bb9a4734a585fe12725b8c2763de0a265aa1e909b485a4e3270b7cfd3e4dbe4b602
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-validator-identifier@npm:7.25.9"
@ -332,6 +357,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-validator-identifier@npm:7.27.1"
checksum: 10c0/c558f11c4871d526498e49d07a84752d1800bf72ac0d3dad100309a2eaba24efbf56ea59af5137ff15e3a00280ebe588560534b0e894a4750f8b1411d8f78b84
languageName: node
linkType: hard
"@babel/helper-validator-option@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-validator-option@npm:7.25.9"
@ -349,6 +381,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.25.4":
version: 7.27.2
resolution: "@babel/parser@npm:7.27.2"
dependencies:
"@babel/types": "npm:^7.27.1"
bin:
parser: ./bin/babel-parser.js
checksum: 10c0/3c06692768885c2f58207fc8c2cbdb4a44df46b7d93135a083f6eaa49310f7ced490ce76043a2a7606cdcc13f27e3d835e141b692f2f6337a2e7f43c1dbb04b4
languageName: node
linkType: hard
"@babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/parser@npm:7.27.0"
@ -371,7 +414,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.18.6":
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.6":
version: 7.27.1
resolution: "@babel/runtime@npm:7.27.1"
checksum: 10c0/530a7332f86ac5a7442250456823a930906911d895c0b743bf1852efc88a20a016ed4cd26d442d0ca40ae6d5448111e02a08dd638a4f1064b47d080e2875dc05
@ -413,6 +456,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/types@npm:7.27.1"
dependencies:
"@babel/helper-string-parser": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.27.1"
checksum: 10c0/ed736f14db2fdf0d36c539c8e06b6bb5e8f9649a12b5c0e1c516fed827f27ef35085abe08bf4d1302a4e20c9a254e762eed453bce659786d4a6e01ba26a91377
languageName: node
linkType: hard
"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/types@npm:7.27.0"
@ -423,6 +476,13 @@ __metadata:
languageName: node
linkType: hard
"@bcoe/v8-coverage@npm:^1.0.2":
version: 1.0.2
resolution: "@bcoe/v8-coverage@npm:1.0.2"
checksum: 10c0/1eb1dc93cc17fb7abdcef21a6e7b867d6aa99a7ec88ec8207402b23d9083ab22a8011213f04b2cf26d535f1d22dc26139b7929e6c2134c254bd1e14ba5e678c3
languageName: node
linkType: hard
"@braintree/sanitize-url@npm:^7.0.4":
version: 7.1.1
resolution: "@braintree/sanitize-url@npm:7.1.1"
@ -2063,6 +2123,13 @@ __metadata:
languageName: node
linkType: hard
"@istanbuljs/schema@npm:^0.1.2":
version: 0.1.3
resolution: "@istanbuljs/schema@npm:0.1.3"
checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a
languageName: node
linkType: hard
"@jimp/bmp@npm:^0.16.13":
version: 0.16.13
resolution: "@jimp/bmp@npm:0.16.13"
@ -2519,7 +2586,7 @@ __metadata:
languageName: node
linkType: hard
"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
version: 0.3.25
resolution: "@jridgewell/trace-mapping@npm:0.3.25"
dependencies:
@ -3614,6 +3681,17 @@ __metadata:
languageName: node
linkType: hard
"@playwright/test@npm:^1.52.0":
version: 1.52.0
resolution: "@playwright/test@npm:1.52.0"
dependencies:
playwright: "npm:1.52.0"
bin:
playwright: cli.js
checksum: 10c0/1c428b421593eb4f79b7c99783a389c3ab3526c9051ec772749f4fca61414dfa9f2344eba846faac5f238084aa96c836364a91d81d3034ac54924f239a93e247
languageName: node
linkType: hard
"@polka/url@npm:^1.0.0-next.24":
version: 1.0.0-next.29
resolution: "@polka/url@npm:1.0.0-next.29"
@ -4261,6 +4339,66 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/dom@npm:^10.4.0":
version: 10.4.0
resolution: "@testing-library/dom@npm:10.4.0"
dependencies:
"@babel/code-frame": "npm:^7.10.4"
"@babel/runtime": "npm:^7.12.5"
"@types/aria-query": "npm:^5.0.1"
aria-query: "npm:5.3.0"
chalk: "npm:^4.1.0"
dom-accessibility-api: "npm:^0.5.9"
lz-string: "npm:^1.5.0"
pretty-format: "npm:^27.0.2"
checksum: 10c0/0352487720ecd433400671e773df0b84b8268fb3fe8e527cdfd7c11b1365b398b4e0eddba6e7e0c85e8d615f48257753283fccec41f6b986fd6c85f15eb5f84f
languageName: node
linkType: hard
"@testing-library/jest-dom@npm:^6.6.3":
version: 6.6.3
resolution: "@testing-library/jest-dom@npm:6.6.3"
dependencies:
"@adobe/css-tools": "npm:^4.4.0"
aria-query: "npm:^5.0.0"
chalk: "npm:^3.0.0"
css.escape: "npm:^1.5.1"
dom-accessibility-api: "npm:^0.6.3"
lodash: "npm:^4.17.21"
redent: "npm:^3.0.0"
checksum: 10c0/5566b6c0b7b0709bc244aec3aa3dc9e5f4663e8fb2b99d8cd456fc07279e59db6076cbf798f9d3099a98fca7ef4cd50e4e1f4c4dec5a60a8fad8d24a638a5bf6
languageName: node
linkType: hard
"@testing-library/react@npm:^16.3.0":
version: 16.3.0
resolution: "@testing-library/react@npm:16.3.0"
dependencies:
"@babel/runtime": "npm:^7.12.5"
peerDependencies:
"@testing-library/dom": ^10.0.0
"@types/react": ^18.0.0 || ^19.0.0
"@types/react-dom": ^18.0.0 || ^19.0.0
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10c0/3a2cb1f87c9a67e1ebbbcfd99b94b01e496fc35147be8bc5d8bf07a699c7d523a09d57ef2f7b1d91afccd1a28e21eda3b00d80187fbb51b1de01e422592d845e
languageName: node
linkType: hard
"@testing-library/user-event@npm:^14.6.1":
version: 14.6.1
resolution: "@testing-library/user-event@npm:14.6.1"
peerDependencies:
"@testing-library/dom": ">=7.21.4"
checksum: 10c0/75fea130a52bf320d35d46ed54f3eec77e71a56911b8b69a3fe29497b0b9947b2dc80d30f04054ad4ce7f577856ae3e5397ea7dff0ef14944d3909784c7a93fe
languageName: node
linkType: hard
"@tokenizer/token@npm:^0.3.0":
version: 0.3.0
resolution: "@tokenizer/token@npm:0.3.0"
@ -4295,6 +4433,13 @@ __metadata:
languageName: node
linkType: hard
"@types/aria-query@npm:^5.0.1":
version: 5.0.4
resolution: "@types/aria-query@npm:5.0.4"
checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08
languageName: node
linkType: hard
"@types/cacheable-request@npm:^6.0.1":
version: 6.0.3
resolution: "@types/cacheable-request@npm:6.0.3"
@ -5546,23 +5691,76 @@ __metadata:
languageName: node
linkType: hard
"@vitest/expect@npm:3.1.1":
version: 3.1.1
resolution: "@vitest/expect@npm:3.1.1"
"@vitest/browser@npm:^3.1.4":
version: 3.1.4
resolution: "@vitest/browser@npm:3.1.4"
dependencies:
"@vitest/spy": "npm:3.1.1"
"@vitest/utils": "npm:3.1.1"
chai: "npm:^5.2.0"
"@testing-library/dom": "npm:^10.4.0"
"@testing-library/user-event": "npm:^14.6.1"
"@vitest/mocker": "npm:3.1.4"
"@vitest/utils": "npm:3.1.4"
magic-string: "npm:^0.30.17"
sirv: "npm:^3.0.1"
tinyrainbow: "npm:^2.0.0"
checksum: 10c0/ef4528d0ebb89eb3cc044cf597d051c35df8471bb6ba4029e9b3412aa69d0d85a0ce4eb49531fc78fe1ebd97e6428260463068cc96a8d8c1a80150dedfd1ab3a
ws: "npm:^8.18.1"
peerDependencies:
playwright: "*"
vitest: 3.1.4
webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0
peerDependenciesMeta:
playwright:
optional: true
safaridriver:
optional: true
webdriverio:
optional: true
checksum: 10c0/e946141f86aa4eac4bff1c99258e2d0b1710fa37bf769799fb76a9835ffab65f4a0082dcc4fe928bd939d4795e0ef0a9c60818b05e710945995c6fa3373f484a
languageName: node
linkType: hard
"@vitest/mocker@npm:3.1.1":
version: 3.1.1
resolution: "@vitest/mocker@npm:3.1.1"
"@vitest/coverage-v8@npm:^3.1.4":
version: 3.1.4
resolution: "@vitest/coverage-v8@npm:3.1.4"
dependencies:
"@vitest/spy": "npm:3.1.1"
"@ampproject/remapping": "npm:^2.3.0"
"@bcoe/v8-coverage": "npm:^1.0.2"
debug: "npm:^4.4.0"
istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1"
istanbul-lib-source-maps: "npm:^5.0.6"
istanbul-reports: "npm:^3.1.7"
magic-string: "npm:^0.30.17"
magicast: "npm:^0.3.5"
std-env: "npm:^3.9.0"
test-exclude: "npm:^7.0.1"
tinyrainbow: "npm:^2.0.0"
peerDependencies:
"@vitest/browser": 3.1.4
vitest: 3.1.4
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10c0/e2073c06254772bfcaf00e40b76599aa9a3d66fc84c3d980941c4d216b4cf3db8b6f7f0ebcd4905c4ca08e8d19b505d9c428363e51a80a2d653a4a510b280e41
languageName: node
linkType: hard
"@vitest/expect@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/expect@npm:3.1.4"
dependencies:
"@vitest/spy": "npm:3.1.4"
"@vitest/utils": "npm:3.1.4"
chai: "npm:^5.2.0"
tinyrainbow: "npm:^2.0.0"
checksum: 10c0/9cfd7eb6d965a179b4ec0610a9c08b14dc97dbaf81925c8209a054f7a2a3d1eef59fa5e5cd4dd9bf8cb940d85aee5f5102555511a94be9933faf4cc734462a16
languageName: node
linkType: hard
"@vitest/mocker@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/mocker@npm:3.1.4"
dependencies:
"@vitest/spy": "npm:3.1.4"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.17"
peerDependencies:
@ -5573,85 +5771,85 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10c0/9264558809e2d7c77ae9ceefad521dc5f886a567aaf0bdd021b73089b8906ffd92c893f3998d16814f38fc653c7413836f508712355c87749a0e86c7d435eec1
checksum: 10c0/d0b89e3974830d3893e7b8324a77ffeb9436db0969b57c01e2508ebd5b374c9d01f73796c8df8f555a3b1e1b502d40e725f159cd85966eebd3145b2f52e605e2
languageName: node
linkType: hard
"@vitest/pretty-format@npm:3.1.1, @vitest/pretty-format@npm:^3.1.1":
version: 3.1.1
resolution: "@vitest/pretty-format@npm:3.1.1"
"@vitest/pretty-format@npm:3.1.4, @vitest/pretty-format@npm:^3.1.4":
version: 3.1.4
resolution: "@vitest/pretty-format@npm:3.1.4"
dependencies:
tinyrainbow: "npm:^2.0.0"
checksum: 10c0/540cd46d317fc80298c93b185f3fb48dfe90eaaa3942fd700fde6e88d658772c01b56ad5b9b36e4ac368a02e0fc8e0dc72bbdd6dd07a5d75e89ef99c8df5ba6e
checksum: 10c0/11e133640435822b8b8528be540b3d66c1de27ebc2dcf1de87608b7f01a44d15302c4d4bf8330fa848a435450d88a09d7e9442747a5739ae5f500ccdd1493159
languageName: node
linkType: hard
"@vitest/runner@npm:3.1.1":
version: 3.1.1
resolution: "@vitest/runner@npm:3.1.1"
"@vitest/runner@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/runner@npm:3.1.4"
dependencies:
"@vitest/utils": "npm:3.1.1"
"@vitest/utils": "npm:3.1.4"
pathe: "npm:^2.0.3"
checksum: 10c0/35a541069c3c94a2dd02fca2d70cc8d5e66ba2e891cfb80da354174f510aeb96774ffb34fff39cecde9d5c969be4dd20e240a900beb9b225b7512a615ecc5503
checksum: 10c0/efb7512eebd3d786baa617eab332ec9ca6ce62eb1c9dd3945019f7510d745b3cd0fc2978868d792050905aacbf158eefc132359c83e61f0398b46be566013ee6
languageName: node
linkType: hard
"@vitest/snapshot@npm:3.1.1":
version: 3.1.1
resolution: "@vitest/snapshot@npm:3.1.1"
"@vitest/snapshot@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/snapshot@npm:3.1.4"
dependencies:
"@vitest/pretty-format": "npm:3.1.1"
"@vitest/pretty-format": "npm:3.1.4"
magic-string: "npm:^0.30.17"
pathe: "npm:^2.0.3"
checksum: 10c0/43e5fc5db580f20903eb1493d07f08752df8864f7b9b7293a202b2ffe93d8c196a5614d66dda096c6bacc16e12f1836f33ba41898812af6d32676d1eb501536a
checksum: 10c0/ce9d51e1b03e4f91ffad160c570991a8a3c603cb7dc2a9020e58c012e62dccbe2c6ee45e1a1d8489e265b4485c6721eb73b5e91404d1c76da08dcd663f4e18d1
languageName: node
linkType: hard
"@vitest/spy@npm:3.1.1":
version: 3.1.1
resolution: "@vitest/spy@npm:3.1.1"
"@vitest/spy@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/spy@npm:3.1.4"
dependencies:
tinyspy: "npm:^3.0.2"
checksum: 10c0/896659d4b42776cfa2057a1da2c33adbd3f2ebd28005ca606d1616d08d2e726dc1460fb37f1ea7f734756b5bccf926c7165f410e63f0a3b8d992eb5489528b08
checksum: 10c0/747914ac18efa82d75349b0fb0ad8a5e2af6e04f5bbb50a980c9270dd8958f9ddf84cee0849a54e1645af088fc1f709add94a35e99cb14aca2cdb322622ba501
languageName: node
linkType: hard
"@vitest/ui@npm:^3.1.1":
version: 3.1.1
resolution: "@vitest/ui@npm:3.1.1"
"@vitest/ui@npm:^3.1.4":
version: 3.1.4
resolution: "@vitest/ui@npm:3.1.4"
dependencies:
"@vitest/utils": "npm:3.1.1"
"@vitest/utils": "npm:3.1.4"
fflate: "npm:^0.8.2"
flatted: "npm:^3.3.3"
pathe: "npm:^2.0.3"
sirv: "npm:^3.0.1"
tinyglobby: "npm:^0.2.12"
tinyglobby: "npm:^0.2.13"
tinyrainbow: "npm:^2.0.0"
peerDependencies:
vitest: 3.1.1
checksum: 10c0/03bd014a4afa2c4cd6007d8000d881c653414f30d275fe35067b3d50c8a07b9f53cb2a294a8d36adaece7e4671030f90bd51aedb412d64479b981e051e7996ba
vitest: 3.1.4
checksum: 10c0/02dd00e92f73aa0b71f69a374a7f991f16da13d3cec044f341b59e29209ad6197e2b9733b15f2a1b32ef77e1a9d5069eeb574035c3cea749ac2800df7ea23698
languageName: node
linkType: hard
"@vitest/utils@npm:3.1.1":
version: 3.1.1
resolution: "@vitest/utils@npm:3.1.1"
"@vitest/utils@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/utils@npm:3.1.4"
dependencies:
"@vitest/pretty-format": "npm:3.1.1"
"@vitest/pretty-format": "npm:3.1.4"
loupe: "npm:^3.1.3"
tinyrainbow: "npm:^2.0.0"
checksum: 10c0/a9cfe0c0f095b58644ce3ba08309de5be8564c10dad9e62035bd378e60b2834e6a256e6e4ded7dcf027fdc2371301f7965040ad3e6323b747d5b3abbb7ceb0d6
checksum: 10c0/78f1691a2dd578862b236f4962815e7475e547f006e7303a149dc5f910cc1ce6e0bdcbd7b4fd618122d62ca2dcc28bae464d31543f3898f5d88fa35017e00a95
languageName: node
linkType: hard
"@vitest/web-worker@npm:^3.1.3":
version: 3.1.3
resolution: "@vitest/web-worker@npm:3.1.3"
"@vitest/web-worker@npm:^3.1.4":
version: 3.1.4
resolution: "@vitest/web-worker@npm:3.1.4"
dependencies:
debug: "npm:^4.4.0"
peerDependencies:
vitest: 3.1.3
checksum: 10c0/8b8f46f55da0d99716a88d9017d233a4710696405bcd138a5374370b2ce880f71108884a9267905d867d28fcdd1363cf894ef0d9e17268b1d2ff65dc5f706ea6
vitest: 3.1.4
checksum: 10c0/dd883a6a52ca9efd63e5055e87c2e61f0b0fe1cfa95472453ee3db5c3880796aa38042ca968000c14020d410840accbb38ca045ca0c9312c3c7b9b57e0e01a36
languageName: node
linkType: hard
@ -5750,11 +5948,15 @@ __metadata:
"@modelcontextprotocol/sdk": "npm:^1.11.4"
"@mozilla/readability": "npm:^0.6.0"
"@notionhq/client": "npm:^2.2.15"
"@playwright/test": "npm:^1.52.0"
"@reduxjs/toolkit": "npm:^2.2.5"
"@shikijs/markdown-it": "npm:^3.4.2"
"@strongtz/win32-arm64-msvc": "npm:^0.4.7"
"@swc/plugin-styled-components": "npm:^7.1.5"
"@tanstack/react-query": "npm:^5.27.0"
"@testing-library/dom": "npm:^10.4.0"
"@testing-library/jest-dom": "npm:^6.6.3"
"@testing-library/react": "npm:^16.3.0"
"@tryfabric/martian": "npm:^1.2.4"
"@types/diff": "npm:^7"
"@types/fs-extra": "npm:^11"
@ -5773,8 +5975,10 @@ __metadata:
"@uiw/codemirror-themes-all": "npm:^4.23.12"
"@uiw/react-codemirror": "npm:^4.23.12"
"@vitejs/plugin-react-swc": "npm:^3.9.0"
"@vitest/ui": "npm:^3.1.1"
"@vitest/web-worker": "npm:^3.1.3"
"@vitest/browser": "npm:^3.1.4"
"@vitest/coverage-v8": "npm:^3.1.4"
"@vitest/ui": "npm:^3.1.4"
"@vitest/web-worker": "npm:^3.1.4"
"@xyflow/react": "npm:^12.4.4"
antd: "npm:^5.22.5"
archiver: "npm:^7.0.1"
@ -5811,6 +6015,7 @@ __metadata:
html-to-image: "npm:^1.11.13"
husky: "npm:^9.1.7"
i18next: "npm:^23.11.5"
jest-styled-components: "npm:^7.2.0"
jsdom: "npm:^26.0.0"
lint-staged: "npm:^15.5.0"
lodash: "npm:^4.17.21"
@ -5826,6 +6031,7 @@ __metadata:
openai: "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch"
os-proxy-config: "npm:^1.1.2"
p-queue: "npm:^8.1.0"
playwright: "npm:^1.52.0"
prettier: "npm:^3.5.3"
proxy-agent: "npm:^6.5.0"
rc-virtual-list: "npm:^3.18.6"
@ -5861,7 +6067,7 @@ __metadata:
typescript: "npm:^5.6.2"
uuid: "npm:^10.0.0"
vite: "npm:6.2.6"
vitest: "npm:^3.1.1"
vitest: "npm:^3.1.4"
webdav: "npm:^5.8.0"
ws: "npm:^8.18.1"
zipread: "npm:^1.3.3"
@ -6317,6 +6523,22 @@ __metadata:
languageName: node
linkType: hard
"aria-query@npm:5.3.0":
version: 5.3.0
resolution: "aria-query@npm:5.3.0"
dependencies:
dequal: "npm:^2.0.3"
checksum: 10c0/2bff0d4eba5852a9dd578ecf47eaef0e82cc52569b48469b0aac2db5145db0b17b7a58d9e01237706d1e14b7a1b0ac9b78e9c97027ad97679dd8f91b85da1469
languageName: node
linkType: hard
"aria-query@npm:^5.0.0":
version: 5.3.2
resolution: "aria-query@npm:5.3.2"
checksum: 10c0/003c7e3e2cff5540bf7a7893775fc614de82b0c5dde8ae823d47b7a28a9d4da1f7ed85f340bdb93d5649caa927755f0e31ecc7ab63edfdfc00c8ef07e505e03e
languageName: node
linkType: hard
"array-union@npm:^2.1.0":
version: 2.1.0
resolution: "array-union@npm:2.1.0"
@ -6949,6 +7171,16 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^3.0.0":
version: 3.0.0
resolution: "chalk@npm:3.0.0"
dependencies:
ansi-styles: "npm:^4.1.0"
supports-color: "npm:^7.1.0"
checksum: 10c0/ee650b0a065b3d7a6fda258e75d3a86fc8e4effa55871da730a9e42ccb035bf5fd203525e5a1ef45ec2582ecc4f65b47eb11357c526b84dd29a14fb162c414d2
languageName: node
linkType: hard
"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
@ -7655,6 +7887,13 @@ __metadata:
languageName: node
linkType: hard
"css.escape@npm:^1.5.1":
version: 1.5.1
resolution: "css.escape@npm:1.5.1"
checksum: 10c0/5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525
languageName: node
linkType: hard
"cssstyle@npm:^4.2.1":
version: 4.3.0
resolution: "cssstyle@npm:4.3.0"
@ -8403,7 +8642,7 @@ __metadata:
languageName: node
linkType: hard
"dequal@npm:^2.0.0":
"dequal@npm:^2.0.0, dequal@npm:^2.0.3":
version: 2.0.3
resolution: "dequal@npm:2.0.3"
checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888
@ -8550,6 +8789,20 @@ __metadata:
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.5.9":
version: 0.5.16
resolution: "dom-accessibility-api@npm:0.5.16"
checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.6.3":
version: 0.6.3
resolution: "dom-accessibility-api@npm:0.6.3"
checksum: 10c0/10bee5aa514b2a9a37c87cd81268db607a2e933a050074abc2f6fa3da9080ebed206a320cbc123567f2c3087d22292853bdfdceaffdd4334ffe2af9510b29360
languageName: node
linkType: hard
"dom-serializer@npm:^2.0.0":
version: 2.0.0
resolution: "dom-serializer@npm:2.0.0"
@ -9002,10 +9255,10 @@ __metadata:
languageName: node
linkType: hard
"es-module-lexer@npm:^1.6.0":
version: 1.6.0
resolution: "es-module-lexer@npm:1.6.0"
checksum: 10c0/667309454411c0b95c476025929881e71400d74a746ffa1ff4cb450bd87f8e33e8eef7854d68e401895039ac0bac64e7809acbebb6253e055dd49ea9e3ea9212
"es-module-lexer@npm:^1.7.0":
version: 1.7.0
resolution: "es-module-lexer@npm:1.7.0"
checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b
languageName: node
linkType: hard
@ -9752,7 +10005,7 @@ __metadata:
languageName: node
linkType: hard
"expect-type@npm:^1.2.0":
"expect-type@npm:^1.2.1":
version: 1.2.1
resolution: "expect-type@npm:1.2.1"
checksum: 10c0/b775c9adab3c190dd0d398c722531726cdd6022849b4adba19dceab58dda7e000a7c6c872408cd73d665baa20d381eca36af4f7b393a4ba60dd10232d1fb8898
@ -9971,7 +10224,7 @@ __metadata:
languageName: node
linkType: hard
"fdir@npm:^6.4.3":
"fdir@npm:^6.4.3, fdir@npm:^6.4.4":
version: 6.4.4
resolution: "fdir@npm:6.4.4"
peerDependencies:
@ -10372,6 +10625,16 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:2.3.2":
version: 2.3.2
resolution: "fsevents@npm:2.3.2"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
@ -10382,6 +10645,15 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
dependencies:
node-gyp: "npm:latest"
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
@ -10598,7 +10870,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^10.0.0, glob@npm:^10.3.12, glob@npm:^10.3.7":
"glob@npm:^10.0.0, glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1":
version: 10.4.5
resolution: "glob@npm:10.4.5"
dependencies:
@ -11130,6 +11402,13 @@ __metadata:
languageName: node
linkType: hard
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0
languageName: node
linkType: hard
"html-parse-stringify@npm:^3.0.1":
version: 3.0.1
resolution: "html-parse-stringify@npm:3.0.1"
@ -11848,6 +12127,45 @@ __metadata:
languageName: node
linkType: hard
"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2":
version: 3.2.2
resolution: "istanbul-lib-coverage@npm:3.2.2"
checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b
languageName: node
linkType: hard
"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1":
version: 3.0.1
resolution: "istanbul-lib-report@npm:3.0.1"
dependencies:
istanbul-lib-coverage: "npm:^3.0.0"
make-dir: "npm:^4.0.0"
supports-color: "npm:^7.1.0"
checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7
languageName: node
linkType: hard
"istanbul-lib-source-maps@npm:^5.0.6":
version: 5.0.6
resolution: "istanbul-lib-source-maps@npm:5.0.6"
dependencies:
"@jridgewell/trace-mapping": "npm:^0.3.23"
debug: "npm:^4.1.1"
istanbul-lib-coverage: "npm:^3.0.0"
checksum: 10c0/ffe75d70b303a3621ee4671554f306e0831b16f39ab7f4ab52e54d356a5d33e534d97563e318f1333a6aae1d42f91ec49c76b6cd3f3fb378addcb5c81da0255f
languageName: node
linkType: hard
"istanbul-reports@npm:^3.1.7":
version: 3.1.7
resolution: "istanbul-reports@npm:3.1.7"
dependencies:
html-escaper: "npm:^2.0.0"
istanbul-lib-report: "npm:^3.0.0"
checksum: 10c0/a379fadf9cf8dc5dfe25568115721d4a7eb82fbd50b005a6672aff9c6989b20cc9312d7865814e0859cd8df58cbf664482e1d3604be0afde1f7fc3ccc1394a51
languageName: node
linkType: hard
"jackspeak@npm:^3.1.2":
version: 3.4.3
resolution: "jackspeak@npm:3.4.3"
@ -11875,6 +12193,17 @@ __metadata:
languageName: node
linkType: hard
"jest-styled-components@npm:^7.2.0":
version: 7.2.0
resolution: "jest-styled-components@npm:7.2.0"
dependencies:
"@adobe/css-tools": "npm:^4.0.1"
peerDependencies:
styled-components: ">= 5"
checksum: 10c0/44eecf73cd1ee50686c9c16517222e2c012422dd7d90a07813f82f3ccce4059563e620d25aed60274e05d31fe6375b1f31a3486033aa39ea5e82fd94afcfa32f
languageName: node
linkType: hard
"jimp@npm:^0.16.1":
version: 0.16.13
resolution: "jimp@npm:0.16.13"
@ -12743,6 +13072,15 @@ __metadata:
languageName: node
linkType: hard
"lz-string@npm:^1.5.0":
version: 1.5.0
resolution: "lz-string@npm:1.5.0"
bin:
lz-string: bin/bin.js
checksum: 10c0/36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b
languageName: node
linkType: hard
"mac-system-proxy@npm:^1.0.0":
version: 1.0.4
resolution: "mac-system-proxy@npm:1.0.4"
@ -12759,6 +13097,17 @@ __metadata:
languageName: node
linkType: hard
"magicast@npm:^0.3.5":
version: 0.3.5
resolution: "magicast@npm:0.3.5"
dependencies:
"@babel/parser": "npm:^7.25.4"
"@babel/types": "npm:^7.25.4"
source-map-js: "npm:^1.2.0"
checksum: 10c0/a6cacc0a848af84f03e3f5bda7b0de75e4d0aa9ddce5517fd23ed0f31b5ddd51b2d0ff0b7e09b51f7de0f4053c7a1107117edda6b0732dca3e9e39e6c5a68c64
languageName: node
linkType: hard
"make-dir@npm:^1.0.0":
version: 1.3.0
resolution: "make-dir@npm:1.3.0"
@ -12768,6 +13117,15 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^4.0.0":
version: 4.0.0
resolution: "make-dir@npm:4.0.0"
dependencies:
semver: "npm:^7.5.3"
checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68
languageName: node
linkType: hard
"make-fetch-happen@npm:^10.0.3, make-fetch-happen@npm:^10.2.1":
version: 10.2.1
resolution: "make-fetch-happen@npm:10.2.1"
@ -13947,6 +14305,13 @@ __metadata:
languageName: node
linkType: hard
"min-indent@npm:^1.0.0":
version: 1.0.1
resolution: "min-indent@npm:1.0.1"
checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c
languageName: node
linkType: hard
"minimalistic-assert@npm:^1.0.1":
version: 1.0.1
resolution: "minimalistic-assert@npm:1.0.1"
@ -15373,6 +15738,30 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.52.0":
version: 1.52.0
resolution: "playwright-core@npm:1.52.0"
bin:
playwright-core: cli.js
checksum: 10c0/640945507e6ca2144e9f596b2a6ecac042c2fd3683ff99e6271e9a7b38f3602d415f282609d569456f66680aab8b3c5bb1b257d8fb63a7fc0ed648261110421f
languageName: node
linkType: hard
"playwright@npm:1.52.0, playwright@npm:^1.52.0":
version: 1.52.0
resolution: "playwright@npm:1.52.0"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.52.0"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10c0/2c6edf1e15e59bbaf77f3fa0fe0ac975793c17cff835d9c8b8bc6395a3b6f1c01898b3058ab37891b2e4d424bcc8f1b4844fe70d943e0143d239d7451408c579
languageName: node
linkType: hard
"plist@npm:3.1.0, plist@npm:^3.0.4, plist@npm:^3.0.5, plist@npm:^3.1.0":
version: 3.1.0
resolution: "plist@npm:3.1.0"
@ -15501,6 +15890,17 @@ __metadata:
languageName: node
linkType: hard
"pretty-format@npm:^27.0.2":
version: 27.5.1
resolution: "pretty-format@npm:27.5.1"
dependencies:
ansi-regex: "npm:^5.0.1"
ansi-styles: "npm:^5.0.0"
react-is: "npm:^17.0.1"
checksum: 10c0/0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed
languageName: node
linkType: hard
"proc-log@npm:^2.0.1":
version: 2.0.1
resolution: "proc-log@npm:2.0.1"
@ -16330,6 +16730,13 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^17.0.1":
version: 17.0.2
resolution: "react-is@npm:17.0.2"
checksum: 10c0/2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053
languageName: node
linkType: hard
"react-is@npm:^18.0.0, react-is@npm:^18.2.0":
version: 18.3.1
resolution: "react-is@npm:18.3.1"
@ -16560,6 +16967,16 @@ __metadata:
languageName: node
linkType: hard
"redent@npm:^3.0.0":
version: 3.0.0
resolution: "redent@npm:3.0.0"
dependencies:
indent-string: "npm:^4.0.0"
strip-indent: "npm:^3.0.0"
checksum: 10c0/d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae
languageName: node
linkType: hard
"redux-persist@npm:^6.0.0":
version: 6.0.0
resolution: "redux-persist@npm:6.0.0"
@ -17618,7 +18035,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.1":
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
@ -17768,7 +18185,7 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^3.8.1":
"std-env@npm:^3.9.0":
version: 3.9.0
resolution: "std-env@npm:3.9.0"
checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50
@ -17954,6 +18371,15 @@ __metadata:
languageName: node
linkType: hard
"strip-indent@npm:^3.0.0":
version: 3.0.0
resolution: "strip-indent@npm:3.0.0"
dependencies:
min-indent: "npm:^1.0.0"
checksum: 10c0/ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679
languageName: node
linkType: hard
"strip-json-comments@npm:^3.1.1":
version: 3.1.1
resolution: "strip-json-comments@npm:3.1.1"
@ -18215,6 +18641,17 @@ __metadata:
languageName: node
linkType: hard
"test-exclude@npm:^7.0.1":
version: 7.0.1
resolution: "test-exclude@npm:7.0.1"
dependencies:
"@istanbuljs/schema": "npm:^0.1.2"
glob: "npm:^10.4.1"
minimatch: "npm:^9.0.4"
checksum: 10c0/6d67b9af4336a2e12b26a68c83308c7863534c65f27ed4ff7068a56f5a58f7ac703e8fc80f698a19bb154fd8f705cdf7ec347d9512b2c522c737269507e7b263
languageName: node
linkType: hard
"text-decoder@npm:^1.1.0":
version: 1.2.3
resolution: "text-decoder@npm:1.2.3"
@ -18343,6 +18780,16 @@ __metadata:
languageName: node
linkType: hard
"tinyglobby@npm:^0.2.13":
version: 0.2.13
resolution: "tinyglobby@npm:0.2.13"
dependencies:
fdir: "npm:^6.4.4"
picomatch: "npm:^4.0.2"
checksum: 10c0/ef07dfaa7b26936601d3f6d999f7928a4d1c6234c5eb36896bb88681947c0d459b7ebe797022400e555fe4b894db06e922b95d0ce60cb05fd827a0a66326b18c
languageName: node
linkType: hard
"tinypool@npm:^1.0.2":
version: 1.0.2
resolution: "tinypool@npm:1.0.2"
@ -19147,18 +19594,18 @@ __metadata:
languageName: node
linkType: hard
"vite-node@npm:3.1.1":
version: 3.1.1
resolution: "vite-node@npm:3.1.1"
"vite-node@npm:3.1.4":
version: 3.1.4
resolution: "vite-node@npm:3.1.4"
dependencies:
cac: "npm:^6.7.14"
debug: "npm:^4.4.0"
es-module-lexer: "npm:^1.6.0"
es-module-lexer: "npm:^1.7.0"
pathe: "npm:^2.0.3"
vite: "npm:^5.0.0 || ^6.0.0"
bin:
vite-node: vite-node.mjs
checksum: 10c0/15ee73c472ae00f042a7cee09a31355d2c0efbb2dab160377545be9ba4b980a5f4cb2841b98319d87bedf630bbbb075e6b40796b39f65610920cf3fde66fdf8d
checksum: 10c0/2fc71ddadd308b19b0d0dc09f5b9a108ea9bb640ec5fbd6179267994da8fd6c9d6a4c92098af7de73a0fa817055b518b28972452a2f19a1be754e79947e289d2
languageName: node
linkType: hard
@ -19269,36 +19716,37 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:^3.1.1":
version: 3.1.1
resolution: "vitest@npm:3.1.1"
"vitest@npm:^3.1.4":
version: 3.1.4
resolution: "vitest@npm:3.1.4"
dependencies:
"@vitest/expect": "npm:3.1.1"
"@vitest/mocker": "npm:3.1.1"
"@vitest/pretty-format": "npm:^3.1.1"
"@vitest/runner": "npm:3.1.1"
"@vitest/snapshot": "npm:3.1.1"
"@vitest/spy": "npm:3.1.1"
"@vitest/utils": "npm:3.1.1"
"@vitest/expect": "npm:3.1.4"
"@vitest/mocker": "npm:3.1.4"
"@vitest/pretty-format": "npm:^3.1.4"
"@vitest/runner": "npm:3.1.4"
"@vitest/snapshot": "npm:3.1.4"
"@vitest/spy": "npm:3.1.4"
"@vitest/utils": "npm:3.1.4"
chai: "npm:^5.2.0"
debug: "npm:^4.4.0"
expect-type: "npm:^1.2.0"
expect-type: "npm:^1.2.1"
magic-string: "npm:^0.30.17"
pathe: "npm:^2.0.3"
std-env: "npm:^3.8.1"
std-env: "npm:^3.9.0"
tinybench: "npm:^2.9.0"
tinyexec: "npm:^0.3.2"
tinyglobby: "npm:^0.2.13"
tinypool: "npm:^1.0.2"
tinyrainbow: "npm:^2.0.0"
vite: "npm:^5.0.0 || ^6.0.0"
vite-node: "npm:3.1.1"
vite-node: "npm:3.1.4"
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
"@types/debug": ^4.1.12
"@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0
"@vitest/browser": 3.1.1
"@vitest/ui": 3.1.1
"@vitest/browser": 3.1.4
"@vitest/ui": 3.1.4
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
@ -19318,7 +19766,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: 10c0/680f31d2a7ca59509f837acdbacd9dff405e1b00c606d7cd29717127c6b543f186055854562c2604f74c5cd668b70174968d28feb4ed948a7e013c9477a68d50
checksum: 10c0/aec575e3cc6cf9b3cee224ae63569479e3a41fa980e495a73d384e31e273f34b18317a0da23bbd577c60fe5e717fa41cdc390de4049ce224ffdaa266ea0cdc67
languageName: node
linkType: hard