refactor(web): migrate to Vitest and esm (#29974)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou
2025-12-22 16:35:22 +08:00
committed by GitHub
parent 42f7ecda12
commit eabdc5f0eb
268 changed files with 5455 additions and 6307 deletions

View File

@@ -66,7 +66,7 @@ describe('app-redirection', () => {
*/
test('calls redirection function with correct path for non-editor', () => {
const app = { id: 'app-123', mode: AppModeEnum.CHAT }
const mockRedirect = jest.fn()
const mockRedirect = vi.fn()
getRedirection(false, app, mockRedirect)
@@ -76,7 +76,7 @@ describe('app-redirection', () => {
test('calls redirection function with workflow path for editor', () => {
const app = { id: 'app-123', mode: AppModeEnum.WORKFLOW }
const mockRedirect = jest.fn()
const mockRedirect = vi.fn()
getRedirection(true, app, mockRedirect)
@@ -86,7 +86,7 @@ describe('app-redirection', () => {
test('calls redirection function with configuration path for chat mode editor', () => {
const app = { id: 'app-123', mode: AppModeEnum.CHAT }
const mockRedirect = jest.fn()
const mockRedirect = vi.fn()
getRedirection(true, app, mockRedirect)

View File

@@ -13,7 +13,7 @@ import { writeTextToClipboard } from './clipboard'
describe('Clipboard Utilities', () => {
describe('writeTextToClipboard', () => {
afterEach(() => {
jest.restoreAllMocks()
vi.restoreAllMocks()
})
/**
@@ -21,7 +21,7 @@ describe('Clipboard Utilities', () => {
* When navigator.clipboard is available, should use the modern API
*/
it('should use navigator.clipboard.writeText when available', async () => {
const mockWriteText = jest.fn().mockResolvedValue(undefined)
const mockWriteText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: mockWriteText },
writable: true,
@@ -44,11 +44,11 @@ describe('Clipboard Utilities', () => {
configurable: true,
})
const mockExecCommand = jest.fn().mockReturnValue(true)
const mockExecCommand = vi.fn().mockReturnValue(true)
document.execCommand = mockExecCommand
const appendChildSpy = jest.spyOn(document.body, 'appendChild')
const removeChildSpy = jest.spyOn(document.body, 'removeChild')
const appendChildSpy = vi.spyOn(document.body, 'appendChild')
const removeChildSpy = vi.spyOn(document.body, 'removeChild')
await writeTextToClipboard('fallback text')
@@ -68,7 +68,7 @@ describe('Clipboard Utilities', () => {
configurable: true,
})
const mockExecCommand = jest.fn().mockReturnValue(false)
const mockExecCommand = vi.fn().mockReturnValue(false)
document.execCommand = mockExecCommand
await expect(writeTextToClipboard('fail text')).rejects.toThrow()
@@ -85,7 +85,7 @@ describe('Clipboard Utilities', () => {
configurable: true,
})
const mockExecCommand = jest.fn().mockImplementation(() => {
const mockExecCommand = vi.fn().mockImplementation(() => {
throw new Error('execCommand error')
})
document.execCommand = mockExecCommand
@@ -104,8 +104,8 @@ describe('Clipboard Utilities', () => {
configurable: true,
})
document.execCommand = jest.fn().mockReturnValue(true)
const removeChildSpy = jest.spyOn(document.body, 'removeChild')
document.execCommand = vi.fn().mockReturnValue(true)
const removeChildSpy = vi.spyOn(document.body, 'removeChild')
await writeTextToClipboard('cleanup test')
@@ -117,7 +117,7 @@ describe('Clipboard Utilities', () => {
* Should handle edge case of empty clipboard content
*/
it('should handle empty string', async () => {
const mockWriteText = jest.fn().mockResolvedValue(undefined)
const mockWriteText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: mockWriteText },
writable: true,
@@ -133,7 +133,7 @@ describe('Clipboard Utilities', () => {
* Should preserve newlines, tabs, quotes, unicode, and emojis
*/
it('should handle special characters', async () => {
const mockWriteText = jest.fn().mockResolvedValue(undefined)
const mockWriteText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: mockWriteText },
writable: true,

View File

@@ -61,7 +61,7 @@ describe('Context Utilities', () => {
})
// Suppress console.error for this test
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
expect(() => {
renderHook(() => useTestContext())
@@ -206,7 +206,7 @@ describe('Context Utilities', () => {
name: 'SelectorTest',
})
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
expect(() => {
renderHook(() => useTestContext())
@@ -241,7 +241,7 @@ describe('Context Utilities', () => {
type TestContextValue = { value: string }
const [, useTestContext] = createCtx<TestContextValue>()
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
expect(() => {
renderHook(() => useTestContext())

View File

@@ -1,16 +1,17 @@
import type { Mock } from 'vitest'
import { searchEmoji } from './emoji'
import { SearchIndex } from 'emoji-mart'
jest.mock('emoji-mart', () => ({
vi.mock('emoji-mart', () => ({
SearchIndex: {
search: jest.fn(),
search: vi.fn(),
},
}))
describe('Emoji Utilities', () => {
describe('searchEmoji', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
it('should return emoji natives for search results', async () => {
@@ -19,28 +20,28 @@ describe('Emoji Utilities', () => {
{ skins: [{ native: '😃' }] },
{ skins: [{ native: '😄' }] },
]
;(SearchIndex.search as jest.Mock).mockResolvedValue(mockEmojis)
;(SearchIndex.search as Mock).mockResolvedValue(mockEmojis)
const result = await searchEmoji('smile')
expect(result).toEqual(['😀', '😃', '😄'])
})
it('should return empty array when no results', async () => {
;(SearchIndex.search as jest.Mock).mockResolvedValue([])
;(SearchIndex.search as Mock).mockResolvedValue([])
const result = await searchEmoji('nonexistent')
expect(result).toEqual([])
})
it('should return empty array when search returns null', async () => {
;(SearchIndex.search as jest.Mock).mockResolvedValue(null)
;(SearchIndex.search as Mock).mockResolvedValue(null)
const result = await searchEmoji('test')
expect(result).toEqual([])
})
it('should handle search with empty string', async () => {
;(SearchIndex.search as jest.Mock).mockResolvedValue([])
;(SearchIndex.search as Mock).mockResolvedValue([])
const result = await searchEmoji('')
expect(result).toEqual([])
@@ -57,7 +58,7 @@ describe('Emoji Utilities', () => {
],
},
]
;(SearchIndex.search as jest.Mock).mockResolvedValue(mockEmojis)
;(SearchIndex.search as Mock).mockResolvedValue(mockEmojis)
const result = await searchEmoji('thumbs')
expect(result).toEqual(['👍'])
@@ -68,7 +69,7 @@ describe('Emoji Utilities', () => {
{ skins: [{ native: '❤️' }] },
{ skins: [{ native: '💙' }] },
]
;(SearchIndex.search as jest.Mock).mockResolvedValue(mockEmojis)
;(SearchIndex.search as Mock).mockResolvedValue(mockEmojis)
const result = await searchEmoji('heart love')
expect(result).toEqual(['❤️', '💙'])

View File

@@ -68,8 +68,8 @@ describe('downloadFile', () => {
const mockUrl = 'blob:mockUrl'
// Mock URL.createObjectURL
const createObjectURLMock = jest.fn().mockReturnValue(mockUrl)
const revokeObjectURLMock = jest.fn()
const createObjectURLMock = vi.fn().mockReturnValue(mockUrl)
const revokeObjectURLMock = vi.fn()
Object.defineProperty(window.URL, 'createObjectURL', { value: createObjectURLMock })
Object.defineProperty(window.URL, 'revokeObjectURL', { value: revokeObjectURLMock })
@@ -77,11 +77,11 @@ describe('downloadFile', () => {
const mockLink = {
href: '',
download: '',
click: jest.fn(),
remove: jest.fn(),
click: vi.fn(),
remove: vi.fn(),
}
const createElementMock = jest.spyOn(document, 'createElement').mockReturnValue(mockLink as any)
const appendChildMock = jest.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
const createElementMock = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as any)
const appendChildMock = vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
return node
})
@@ -99,7 +99,7 @@ describe('downloadFile', () => {
expect(revokeObjectURLMock).toHaveBeenCalledWith(mockUrl)
// Clean up mocks
jest.restoreAllMocks()
vi.restoreAllMocks()
})
})

View File

@@ -1,3 +1,4 @@
import type { Mock } from 'vitest'
import {
asyncRunSafe,
canFindTool,
@@ -50,13 +51,13 @@ describe('getTextWidthWithCanvas', () => {
originalCreateElement = document.createElement
// Mock canvas and context
const measureTextMock = jest.fn().mockReturnValue({ width: 100 })
const getContextMock = jest.fn().mockReturnValue({
const measureTextMock = vi.fn().mockReturnValue({ width: 100 })
const getContextMock = vi.fn().mockReturnValue({
measureText: measureTextMock,
font: '',
})
document.createElement = jest.fn().mockReturnValue({
document.createElement = vi.fn().mockReturnValue({
getContext: getContextMock,
})
})
@@ -73,7 +74,7 @@ describe('getTextWidthWithCanvas', () => {
it('should return 0 if context is not available', () => {
// Override mock for this test
document.createElement = jest.fn().mockReturnValue({
document.createElement = vi.fn().mockReturnValue({
getContext: () => null,
})
@@ -243,6 +244,7 @@ describe('removeSpecificQueryParam', () => {
// Mock window.location using defineProperty to handle URL properly
delete (window as any).location
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: {
...originalLocation,
@@ -252,11 +254,12 @@ describe('removeSpecificQueryParam', () => {
},
})
window.history.replaceState = jest.fn()
window.history.replaceState = vi.fn()
})
afterEach(() => {
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: originalLocation,
})
@@ -266,7 +269,7 @@ describe('removeSpecificQueryParam', () => {
it('should remove a single query parameter', () => {
removeSpecificQueryParam('param2')
expect(window.history.replaceState).toHaveBeenCalledTimes(1)
const replaceStateCall = (window.history.replaceState as jest.Mock).mock.calls[0]
const replaceStateCall = (window.history.replaceState as Mock).mock.calls[0]
expect(replaceStateCall[0]).toBe(null)
expect(replaceStateCall[1]).toBe('')
expect(replaceStateCall[2]).toMatch(/param1=value1/)
@@ -277,7 +280,7 @@ describe('removeSpecificQueryParam', () => {
it('should remove multiple query parameters', () => {
removeSpecificQueryParam(['param1', 'param3'])
expect(window.history.replaceState).toHaveBeenCalledTimes(1)
const replaceStateCall = (window.history.replaceState as jest.Mock).mock.calls[0]
const replaceStateCall = (window.history.replaceState as Mock).mock.calls[0]
expect(replaceStateCall[2]).toMatch(/param2=value2/)
expect(replaceStateCall[2]).not.toMatch(/param1=value1/)
expect(replaceStateCall[2]).not.toMatch(/param3=value3/)
@@ -287,7 +290,7 @@ describe('removeSpecificQueryParam', () => {
removeSpecificQueryParam('nonexistent')
expect(window.history.replaceState).toHaveBeenCalledTimes(1)
const replaceStateCall = (window.history.replaceState as jest.Mock).mock.calls[0]
const replaceStateCall = (window.history.replaceState as Mock).mock.calls[0]
expect(replaceStateCall[2]).toMatch(/param1=value1/)
expect(replaceStateCall[2]).toMatch(/param2=value2/)
expect(replaceStateCall[2]).toMatch(/param3=value3/)
@@ -344,38 +347,38 @@ describe('asyncRunSafe extended', () => {
describe('getTextWidthWithCanvas', () => {
it('should return 0 when canvas context is not available', () => {
const mockGetContext = jest.fn().mockReturnValue(null)
jest.spyOn(document, 'createElement').mockReturnValue({
const mockGetContext = vi.fn().mockReturnValue(null)
vi.spyOn(document, 'createElement').mockReturnValue({
getContext: mockGetContext,
} as any)
const width = getTextWidthWithCanvas('test')
expect(width).toBe(0)
jest.restoreAllMocks()
vi.restoreAllMocks()
})
it('should measure text width with custom font', () => {
const mockMeasureText = jest.fn().mockReturnValue({ width: 123.456 })
const mockMeasureText = vi.fn().mockReturnValue({ width: 123.456 })
const mockContext = {
font: '',
measureText: mockMeasureText,
}
jest.spyOn(document, 'createElement').mockReturnValue({
getContext: jest.fn().mockReturnValue(mockContext),
vi.spyOn(document, 'createElement').mockReturnValue({
getContext: vi.fn().mockReturnValue(mockContext),
} as any)
const width = getTextWidthWithCanvas('test', '16px Arial')
expect(mockContext.font).toBe('16px Arial')
expect(width).toBe(123.46)
jest.restoreAllMocks()
vi.restoreAllMocks()
})
it('should handle empty string', () => {
const mockMeasureText = jest.fn().mockReturnValue({ width: 0 })
jest.spyOn(document, 'createElement').mockReturnValue({
getContext: jest.fn().mockReturnValue({
const mockMeasureText = vi.fn().mockReturnValue({ width: 0 })
vi.spyOn(document, 'createElement').mockReturnValue({
getContext: vi.fn().mockReturnValue({
font: '',
measureText: mockMeasureText,
}),
@@ -384,7 +387,7 @@ describe('getTextWidthWithCanvas', () => {
const width = getTextWidthWithCanvas('')
expect(width).toBe(0)
jest.restoreAllMocks()
vi.restoreAllMocks()
})
})
@@ -451,19 +454,20 @@ describe('fetchWithRetry extended', () => {
expect(result).toBe('success')
})
it('should retry specified number of times', async () => {
let _attempts = 0
it('should return error when promise rejects', async () => {
let attempts = 0
const failingPromise = () => {
_attempts++
attempts++
return Promise.reject(new Error('fail'))
}
await fetchWithRetry(failingPromise(), 3)
// Initial attempt + 3 retries = 4 total attempts
// But the function structure means it will try once, then retry 3 times
const [error] = await fetchWithRetry(failingPromise(), 3)
expect(error).toBeInstanceOf(Error)
expect(error?.message).toBe('fail')
expect(attempts).toBe(1)
})
it('should succeed after retries', async () => {
it('should surface rejection from a settled promise', async () => {
let attempts = 0
const eventuallySucceed = new Promise((resolve, reject) => {
attempts++
@@ -473,8 +477,10 @@ describe('fetchWithRetry extended', () => {
resolve('success')
})
await fetchWithRetry(eventuallySucceed, 3)
// Note: This test may need adjustment based on actual retry logic
const [error] = await fetchWithRetry(eventuallySucceed, 3)
expect(error).toBeInstanceOf(Error)
expect(error?.message).toBe('not yet')
expect(attempts).toBe(1)
})
/*
@@ -565,7 +571,7 @@ describe('removeSpecificQueryParam extended', () => {
})
it('should remove single query parameter', () => {
const mockReplaceState = jest.fn()
const mockReplaceState = vi.fn()
window.history.replaceState = mockReplaceState
removeSpecificQueryParam('param1')
@@ -576,7 +582,7 @@ describe('removeSpecificQueryParam extended', () => {
})
it('should remove multiple query parameters', () => {
const mockReplaceState = jest.fn()
const mockReplaceState = vi.fn()
window.history.replaceState = mockReplaceState
removeSpecificQueryParam(['param1', 'param2'])
@@ -588,7 +594,7 @@ describe('removeSpecificQueryParam extended', () => {
})
it('should preserve other parameters', () => {
const mockReplaceState = jest.fn()
const mockReplaceState = vi.fn()
window.history.replaceState = mockReplaceState
removeSpecificQueryParam('param1')

View File

@@ -68,7 +68,7 @@ describe('navigation', () => {
* Tests that the returned function properly navigates with preserved params
*/
test('returns function that calls router.push with correct path', () => {
const mockRouter = { push: jest.fn() }
const mockRouter = { push: vi.fn() }
const backNav = createBackNavigation(mockRouter, '/datasets/123/documents')
backNav()
@@ -77,7 +77,7 @@ describe('navigation', () => {
})
test('returns function that navigates without params when preserveParams is false', () => {
const mockRouter = { push: jest.fn() }
const mockRouter = { push: vi.fn() }
const backNav = createBackNavigation(mockRouter, '/datasets/123/documents', false)
backNav()
@@ -86,7 +86,7 @@ describe('navigation', () => {
})
test('can be called multiple times', () => {
const mockRouter = { push: jest.fn() }
const mockRouter = { push: vi.fn() }
const backNav = createBackNavigation(mockRouter, '/datasets/123/documents')
backNav()
@@ -257,7 +257,7 @@ describe('navigation', () => {
*/
describe('backToDocuments', () => {
test('creates navigation function with preserved params', () => {
const mockRouter = { push: jest.fn() }
const mockRouter = { push: vi.fn() }
const backNav = datasetNavigation.backToDocuments(mockRouter, 'dataset-123')
backNav()
@@ -271,7 +271,7 @@ describe('navigation', () => {
*/
describe('toDocumentDetail', () => {
test('creates navigation function to document detail', () => {
const mockRouter = { push: jest.fn() }
const mockRouter = { push: vi.fn() }
const navFunc = datasetNavigation.toDocumentDetail(mockRouter, 'dataset-123', 'doc-456')
navFunc()
@@ -285,7 +285,7 @@ describe('navigation', () => {
*/
describe('toDocumentSettings', () => {
test('creates navigation function to document settings', () => {
const mockRouter = { push: jest.fn() }
const mockRouter = { push: vi.fn() }
const navFunc = datasetNavigation.toDocumentSettings(mockRouter, 'dataset-123', 'doc-456')
navFunc()

View File

@@ -2,7 +2,7 @@ import { isSupportMCP } from './plugin-version-feature'
describe('plugin-version-feature', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('isSupportMCP', () => {

View File

@@ -132,11 +132,11 @@ describe('Zod Features', () => {
expect(stringArraySchema.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c'])
})
it('should support promise', () => {
it('should support promise', async () => {
const promiseSchema = z.promise(z.string())
const validPromise = Promise.resolve('success')
expect(promiseSchema.parse(validPromise)).resolves.toBe('success')
await expect(promiseSchema.parse(validPromise)).resolves.toBe('success')
})
it('should support unions', () => {