mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 17:27:16 -05:00
test: adding some web tests (#27792)
This commit is contained in:
@@ -759,4 +759,104 @@ export default translation`
|
||||
expect(result).not.toContain('Zbuduj inteligentnego agenta')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Performance and Scalability', () => {
|
||||
it('should handle large translation files efficiently', async () => {
|
||||
// Create a large translation file with 1000 keys
|
||||
const largeContent = `const translation = {
|
||||
${Array.from({ length: 1000 }, (_, i) => ` key${i}: 'value${i}',`).join('\n')}
|
||||
}
|
||||
|
||||
export default translation`
|
||||
|
||||
fs.writeFileSync(path.join(testEnDir, 'large.ts'), largeContent)
|
||||
|
||||
const startTime = Date.now()
|
||||
const keys = await getKeysFromLanguage('en-US')
|
||||
const endTime = Date.now()
|
||||
|
||||
expect(keys.length).toBe(1000)
|
||||
expect(endTime - startTime).toBeLessThan(1000) // Should complete in under 1 second
|
||||
})
|
||||
|
||||
it('should handle multiple translation files concurrently', async () => {
|
||||
// Create multiple files
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const content = `const translation = {
|
||||
key${i}: 'value${i}',
|
||||
nested${i}: {
|
||||
subkey: 'subvalue'
|
||||
}
|
||||
}
|
||||
|
||||
export default translation`
|
||||
fs.writeFileSync(path.join(testEnDir, `file${i}.ts`), content)
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
const keys = await getKeysFromLanguage('en-US')
|
||||
const endTime = Date.now()
|
||||
|
||||
expect(keys.length).toBe(20) // 10 files * 2 keys each
|
||||
expect(endTime - startTime).toBeLessThan(500)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Unicode and Internationalization', () => {
|
||||
it('should handle Unicode characters in keys and values', async () => {
|
||||
const unicodeContent = `const translation = {
|
||||
'中文键': '中文值',
|
||||
'العربية': 'قيمة',
|
||||
'emoji_😀': 'value with emoji 🎉',
|
||||
'mixed_中文_English': 'mixed value'
|
||||
}
|
||||
|
||||
export default translation`
|
||||
|
||||
fs.writeFileSync(path.join(testEnDir, 'unicode.ts'), unicodeContent)
|
||||
|
||||
const keys = await getKeysFromLanguage('en-US')
|
||||
|
||||
expect(keys).toContain('unicode.中文键')
|
||||
expect(keys).toContain('unicode.العربية')
|
||||
expect(keys).toContain('unicode.emoji_😀')
|
||||
expect(keys).toContain('unicode.mixed_中文_English')
|
||||
})
|
||||
|
||||
it('should handle RTL language files', async () => {
|
||||
const rtlContent = `const translation = {
|
||||
مرحبا: 'Hello',
|
||||
العالم: 'World',
|
||||
nested: {
|
||||
مفتاح: 'key'
|
||||
}
|
||||
}
|
||||
|
||||
export default translation`
|
||||
|
||||
fs.writeFileSync(path.join(testEnDir, 'rtl.ts'), rtlContent)
|
||||
|
||||
const keys = await getKeysFromLanguage('en-US')
|
||||
|
||||
expect(keys).toContain('rtl.مرحبا')
|
||||
expect(keys).toContain('rtl.العالم')
|
||||
expect(keys).toContain('rtl.nested.مفتاح')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Recovery', () => {
|
||||
it('should handle syntax errors in translation files gracefully', async () => {
|
||||
const invalidContent = `const translation = {
|
||||
validKey: 'valid value',
|
||||
invalidKey: 'missing quote,
|
||||
anotherKey: 'another value'
|
||||
}
|
||||
|
||||
export default translation`
|
||||
|
||||
fs.writeFileSync(path.join(testEnDir, 'invalid.ts'), invalidContent)
|
||||
|
||||
await expect(getKeysFromLanguage('en-US')).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -286,4 +286,116 @@ describe('Navigation Utilities', () => {
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/filtered-set/documents?page=1&limit=50&status=active&type=pdf&sort=created_at&order=desc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
test('handles special characters in query parameters', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?keyword=hello%20world&filter=type%3Apdf&tag=%E4%B8%AD%E6%96%87' },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const path = createNavigationPath('/datasets/123/documents')
|
||||
expect(path).toContain('hello+world')
|
||||
expect(path).toContain('type%3Apdf')
|
||||
expect(path).toContain('%E4%B8%AD%E6%96%87')
|
||||
})
|
||||
|
||||
test('handles duplicate query parameters', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?tag=tag1&tag=tag2&tag=tag3' },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const params = extractQueryParams(['tag'])
|
||||
// URLSearchParams.get() returns the first value
|
||||
expect(params.tag).toBe('tag1')
|
||||
})
|
||||
|
||||
test('handles very long query strings', () => {
|
||||
const longValue = 'a'.repeat(1000)
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: `?data=${longValue}` },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const path = createNavigationPath('/datasets/123/documents')
|
||||
expect(path).toContain(longValue)
|
||||
expect(path.length).toBeGreaterThan(1000)
|
||||
})
|
||||
|
||||
test('handles empty string values in query parameters', () => {
|
||||
const path = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: 1,
|
||||
keyword: '',
|
||||
filter: '',
|
||||
sort: 'name',
|
||||
})
|
||||
|
||||
expect(path).toBe('/datasets/123/documents?page=1&sort=name')
|
||||
expect(path).not.toContain('keyword=')
|
||||
expect(path).not.toContain('filter=')
|
||||
})
|
||||
|
||||
test('handles null and undefined values in mergeQueryParams', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=1&limit=10&keyword=test' },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const merged = mergeQueryParams({
|
||||
keyword: null,
|
||||
filter: undefined,
|
||||
sort: 'name',
|
||||
})
|
||||
const result = merged.toString()
|
||||
|
||||
expect(result).toContain('page=1')
|
||||
expect(result).toContain('limit=10')
|
||||
expect(result).not.toContain('keyword')
|
||||
expect(result).toContain('sort=name')
|
||||
})
|
||||
|
||||
test('handles navigation with hash fragments', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=1', hash: '#section-2' },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const path = createNavigationPath('/datasets/123/documents')
|
||||
// Should preserve query params but not hash
|
||||
expect(path).toBe('/datasets/123/documents?page=1')
|
||||
})
|
||||
|
||||
test('handles malformed query strings gracefully', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=1&invalid&limit=10&=value&key=' },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const params = extractQueryParams(['page', 'limit', 'invalid', 'key'])
|
||||
expect(params.page).toBe('1')
|
||||
expect(params.limit).toBe('10')
|
||||
// Malformed params should be handled by URLSearchParams
|
||||
expect(params.invalid).toBe('') // for `&invalid`
|
||||
expect(params.key).toBe('') // for `&key=`
|
||||
})
|
||||
})
|
||||
|
||||
describe('Performance Tests', () => {
|
||||
test('handles large number of query parameters efficiently', () => {
|
||||
const manyParams = Array.from({ length: 50 }, (_, i) => `param${i}=value${i}`).join('&')
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: `?${manyParams}` },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const startTime = Date.now()
|
||||
const path = createNavigationPath('/datasets/123/documents')
|
||||
const endTime = Date.now()
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(50) // Should be fast
|
||||
expect(path).toContain('param0=value0')
|
||||
expect(path).toContain('param49=value49')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,3 +14,39 @@ describe('makeProviderQuery', () => {
|
||||
expect(buildProviderQuery('ABC?DEF')).toBe('provider=ABC%3FDEF')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tools Utilities', () => {
|
||||
describe('buildProviderQuery', () => {
|
||||
it('should build query string with provider parameter', () => {
|
||||
const result = buildProviderQuery('openai')
|
||||
expect(result).toBe('provider=openai')
|
||||
})
|
||||
|
||||
it('should handle provider names with special characters', () => {
|
||||
const result = buildProviderQuery('provider-name')
|
||||
expect(result).toBe('provider=provider-name')
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = buildProviderQuery('')
|
||||
expect(result).toBe('provider=')
|
||||
})
|
||||
|
||||
it('should URL encode special characters', () => {
|
||||
const result = buildProviderQuery('provider name')
|
||||
expect(result).toBe('provider=provider+name')
|
||||
})
|
||||
|
||||
it('should handle Unicode characters', () => {
|
||||
const result = buildProviderQuery('提供者')
|
||||
expect(result).toContain('provider=')
|
||||
expect(decodeURIComponent(result)).toBe('provider=提供者')
|
||||
})
|
||||
|
||||
it('should handle provider names with slashes', () => {
|
||||
const result = buildProviderQuery('langgenius/openai/gpt-4')
|
||||
expect(result).toContain('provider=')
|
||||
expect(decodeURIComponent(result)).toBe('provider=langgenius/openai/gpt-4')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
109
web/utils/clipboard.spec.ts
Normal file
109
web/utils/clipboard.spec.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { writeTextToClipboard } from './clipboard'
|
||||
|
||||
describe('Clipboard Utilities', () => {
|
||||
describe('writeTextToClipboard', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should use navigator.clipboard.writeText when available', async () => {
|
||||
const mockWriteText = jest.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: mockWriteText },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
await writeTextToClipboard('test text')
|
||||
expect(mockWriteText).toHaveBeenCalledWith('test text')
|
||||
})
|
||||
|
||||
it('should fallback to execCommand when clipboard API not available', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
const mockExecCommand = jest.fn().mockReturnValue(true)
|
||||
document.execCommand = mockExecCommand
|
||||
|
||||
const appendChildSpy = jest.spyOn(document.body, 'appendChild')
|
||||
const removeChildSpy = jest.spyOn(document.body, 'removeChild')
|
||||
|
||||
await writeTextToClipboard('fallback text')
|
||||
|
||||
expect(appendChildSpy).toHaveBeenCalled()
|
||||
expect(mockExecCommand).toHaveBeenCalledWith('copy')
|
||||
expect(removeChildSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle execCommand failure', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
const mockExecCommand = jest.fn().mockReturnValue(false)
|
||||
document.execCommand = mockExecCommand
|
||||
|
||||
await expect(writeTextToClipboard('fail text')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should handle execCommand exception', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
const mockExecCommand = jest.fn().mockImplementation(() => {
|
||||
throw new Error('execCommand error')
|
||||
})
|
||||
document.execCommand = mockExecCommand
|
||||
|
||||
await expect(writeTextToClipboard('error text')).rejects.toThrow('execCommand error')
|
||||
})
|
||||
|
||||
it('should clean up textarea after fallback', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
document.execCommand = jest.fn().mockReturnValue(true)
|
||||
const removeChildSpy = jest.spyOn(document.body, 'removeChild')
|
||||
|
||||
await writeTextToClipboard('cleanup test')
|
||||
|
||||
expect(removeChildSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty string', async () => {
|
||||
const mockWriteText = jest.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: mockWriteText },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
await writeTextToClipboard('')
|
||||
expect(mockWriteText).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should handle special characters', async () => {
|
||||
const mockWriteText = jest.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: mockWriteText },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
const specialText = 'Test\n\t"quotes"\n中文\n😀'
|
||||
await writeTextToClipboard(specialText)
|
||||
expect(mockWriteText).toHaveBeenCalledWith(specialText)
|
||||
})
|
||||
})
|
||||
})
|
||||
77
web/utils/emoji.spec.ts
Normal file
77
web/utils/emoji.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { searchEmoji } from './emoji'
|
||||
import { SearchIndex } from 'emoji-mart'
|
||||
|
||||
jest.mock('emoji-mart', () => ({
|
||||
SearchIndex: {
|
||||
search: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('Emoji Utilities', () => {
|
||||
describe('searchEmoji', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return emoji natives for search results', async () => {
|
||||
const mockEmojis = [
|
||||
{ skins: [{ native: '😀' }] },
|
||||
{ skins: [{ native: '😃' }] },
|
||||
{ skins: [{ native: '😄' }] },
|
||||
]
|
||||
;(SearchIndex.search as jest.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([])
|
||||
|
||||
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)
|
||||
|
||||
const result = await searchEmoji('test')
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle search with empty string', async () => {
|
||||
;(SearchIndex.search as jest.Mock).mockResolvedValue([])
|
||||
|
||||
const result = await searchEmoji('')
|
||||
expect(result).toEqual([])
|
||||
expect(SearchIndex.search).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should extract native from first skin', async () => {
|
||||
const mockEmojis = [
|
||||
{
|
||||
skins: [
|
||||
{ native: '👍' },
|
||||
{ native: '👍🏻' },
|
||||
{ native: '👍🏼' },
|
||||
],
|
||||
},
|
||||
]
|
||||
;(SearchIndex.search as jest.Mock).mockResolvedValue(mockEmojis)
|
||||
|
||||
const result = await searchEmoji('thumbs')
|
||||
expect(result).toEqual(['👍'])
|
||||
})
|
||||
|
||||
it('should handle multiple search terms', async () => {
|
||||
const mockEmojis = [
|
||||
{ skins: [{ native: '❤️' }] },
|
||||
{ skins: [{ native: '💙' }] },
|
||||
]
|
||||
;(SearchIndex.search as jest.Mock).mockResolvedValue(mockEmojis)
|
||||
|
||||
const result = await searchEmoji('heart love')
|
||||
expect(result).toEqual(['❤️', '💙'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { downloadFile, formatFileSize, formatNumber, formatTime } from './format'
|
||||
import { downloadFile, formatFileSize, formatNumber, formatNumberAbbreviated, formatTime } from './format'
|
||||
|
||||
describe('formatNumber', () => {
|
||||
test('should correctly format integers', () => {
|
||||
@@ -102,3 +102,95 @@ describe('downloadFile', () => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatNumberAbbreviated', () => {
|
||||
it('should return number as string when less than 1000', () => {
|
||||
expect(formatNumberAbbreviated(0)).toBe('0')
|
||||
expect(formatNumberAbbreviated(1)).toBe('1')
|
||||
expect(formatNumberAbbreviated(999)).toBe('999')
|
||||
})
|
||||
|
||||
it('should format thousands with k suffix', () => {
|
||||
expect(formatNumberAbbreviated(1000)).toBe('1k')
|
||||
expect(formatNumberAbbreviated(1200)).toBe('1.2k')
|
||||
expect(formatNumberAbbreviated(1500)).toBe('1.5k')
|
||||
expect(formatNumberAbbreviated(9999)).toBe('10k')
|
||||
})
|
||||
|
||||
it('should format millions with M suffix', () => {
|
||||
expect(formatNumberAbbreviated(1000000)).toBe('1M')
|
||||
expect(formatNumberAbbreviated(1500000)).toBe('1.5M')
|
||||
expect(formatNumberAbbreviated(2300000)).toBe('2.3M')
|
||||
expect(formatNumberAbbreviated(999999999)).toBe('1000M')
|
||||
})
|
||||
|
||||
it('should format billions with B suffix', () => {
|
||||
expect(formatNumberAbbreviated(1000000000)).toBe('1B')
|
||||
expect(formatNumberAbbreviated(1500000000)).toBe('1.5B')
|
||||
expect(formatNumberAbbreviated(2300000000)).toBe('2.3B')
|
||||
})
|
||||
|
||||
it('should remove .0 from whole numbers', () => {
|
||||
expect(formatNumberAbbreviated(1000)).toBe('1k')
|
||||
expect(formatNumberAbbreviated(2000000)).toBe('2M')
|
||||
expect(formatNumberAbbreviated(3000000000)).toBe('3B')
|
||||
})
|
||||
|
||||
it('should keep decimal for non-whole numbers', () => {
|
||||
expect(formatNumberAbbreviated(1100)).toBe('1.1k')
|
||||
expect(formatNumberAbbreviated(1500000)).toBe('1.5M')
|
||||
expect(formatNumberAbbreviated(2700000000)).toBe('2.7B')
|
||||
})
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(formatNumberAbbreviated(950)).toBe('950')
|
||||
expect(formatNumberAbbreviated(1001)).toBe('1k')
|
||||
expect(formatNumberAbbreviated(999999)).toBe('1000k')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatNumber edge cases', () => {
|
||||
it('should handle very large numbers', () => {
|
||||
expect(formatNumber(1234567890123)).toBe('1,234,567,890,123')
|
||||
})
|
||||
|
||||
it('should handle numbers with many decimal places', () => {
|
||||
expect(formatNumber(1234.56789)).toBe('1,234.56789')
|
||||
})
|
||||
|
||||
it('should handle negative decimals', () => {
|
||||
expect(formatNumber(-1234.56)).toBe('-1,234.56')
|
||||
})
|
||||
|
||||
it('should handle string with decimals', () => {
|
||||
expect(formatNumber('9876543.21')).toBe('9,876,543.21')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatFileSize edge cases', () => {
|
||||
it('should handle exactly 1024 bytes', () => {
|
||||
expect(formatFileSize(1024)).toBe('1.00 KB')
|
||||
})
|
||||
|
||||
it('should handle fractional bytes', () => {
|
||||
expect(formatFileSize(512.5)).toBe('512.50 bytes')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatTime edge cases', () => {
|
||||
it('should handle exactly 60 seconds', () => {
|
||||
expect(formatTime(60)).toBe('1.00 min')
|
||||
})
|
||||
|
||||
it('should handle exactly 3600 seconds', () => {
|
||||
expect(formatTime(3600)).toBe('1.00 h')
|
||||
})
|
||||
|
||||
it('should handle fractional seconds', () => {
|
||||
expect(formatTime(45.5)).toBe('45.50 sec')
|
||||
})
|
||||
|
||||
it('should handle very large durations', () => {
|
||||
expect(formatTime(86400)).toBe('24.00 h') // 24 hours
|
||||
})
|
||||
})
|
||||
|
||||
@@ -293,3 +293,308 @@ describe('removeSpecificQueryParam', () => {
|
||||
expect(replaceStateCall[2]).toMatch(/param3=value3/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sleep', () => {
|
||||
it('should resolve after specified milliseconds', async () => {
|
||||
const start = Date.now()
|
||||
await sleep(100)
|
||||
const end = Date.now()
|
||||
expect(end - start).toBeGreaterThanOrEqual(90) // Allow some tolerance
|
||||
})
|
||||
|
||||
it('should handle zero milliseconds', async () => {
|
||||
await expect(sleep(0)).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('asyncRunSafe extended', () => {
|
||||
it('should handle promise that resolves with null', async () => {
|
||||
const [error, result] = await asyncRunSafe(Promise.resolve(null))
|
||||
expect(error).toBeNull()
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle promise that resolves with undefined', async () => {
|
||||
const [error, result] = await asyncRunSafe(Promise.resolve(undefined))
|
||||
expect(error).toBeNull()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle promise that resolves with false', async () => {
|
||||
const [error, result] = await asyncRunSafe(Promise.resolve(false))
|
||||
expect(error).toBeNull()
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle promise that resolves with 0', async () => {
|
||||
const [error, result] = await asyncRunSafe(Promise.resolve(0))
|
||||
expect(error).toBeNull()
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
|
||||
// TODO: pre-commit blocks this test case
|
||||
// Error msg: "Expected the Promise rejection reason to be an Error"
|
||||
|
||||
// it('should handle promise that rejects with null', async () => {
|
||||
// const [error] = await asyncRunSafe(Promise.reject(null))
|
||||
// expect(error).toBeInstanceOf(Error)
|
||||
// expect(error?.message).toBe('unknown error')
|
||||
// })
|
||||
})
|
||||
|
||||
describe('getTextWidthWithCanvas', () => {
|
||||
it('should return 0 when canvas context is not available', () => {
|
||||
const mockGetContext = jest.fn().mockReturnValue(null)
|
||||
jest.spyOn(document, 'createElement').mockReturnValue({
|
||||
getContext: mockGetContext,
|
||||
} as any)
|
||||
|
||||
const width = getTextWidthWithCanvas('test')
|
||||
expect(width).toBe(0)
|
||||
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should measure text width with custom font', () => {
|
||||
const mockMeasureText = jest.fn().mockReturnValue({ width: 123.456 })
|
||||
const mockContext = {
|
||||
font: '',
|
||||
measureText: mockMeasureText,
|
||||
}
|
||||
jest.spyOn(document, 'createElement').mockReturnValue({
|
||||
getContext: jest.fn().mockReturnValue(mockContext),
|
||||
} as any)
|
||||
|
||||
const width = getTextWidthWithCanvas('test', '16px Arial')
|
||||
expect(mockContext.font).toBe('16px Arial')
|
||||
expect(width).toBe(123.46)
|
||||
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const mockMeasureText = jest.fn().mockReturnValue({ width: 0 })
|
||||
jest.spyOn(document, 'createElement').mockReturnValue({
|
||||
getContext: jest.fn().mockReturnValue({
|
||||
font: '',
|
||||
measureText: mockMeasureText,
|
||||
}),
|
||||
} as any)
|
||||
|
||||
const width = getTextWidthWithCanvas('')
|
||||
expect(width).toBe(0)
|
||||
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
|
||||
describe('randomString extended', () => {
|
||||
it('should generate string of exact length', () => {
|
||||
expect(randomString(10).length).toBe(10)
|
||||
expect(randomString(50).length).toBe(50)
|
||||
expect(randomString(100).length).toBe(100)
|
||||
})
|
||||
|
||||
it('should generate different strings on multiple calls', () => {
|
||||
const str1 = randomString(20)
|
||||
const str2 = randomString(20)
|
||||
const str3 = randomString(20)
|
||||
expect(str1).not.toBe(str2)
|
||||
expect(str2).not.toBe(str3)
|
||||
expect(str1).not.toBe(str3)
|
||||
})
|
||||
|
||||
it('should only contain valid characters', () => {
|
||||
const validChars = /^[0-9a-zA-Z_-]+$/
|
||||
const str = randomString(100)
|
||||
expect(validChars.test(str)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle length of 1', () => {
|
||||
const str = randomString(1)
|
||||
expect(str.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should handle length of 0', () => {
|
||||
const str = randomString(0)
|
||||
expect(str).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPurifyHref extended', () => {
|
||||
it('should escape HTML entities', () => {
|
||||
expect(getPurifyHref('<script>alert(1)</script>')).not.toContain('<script>')
|
||||
expect(getPurifyHref('test&test')).toContain('&')
|
||||
expect(getPurifyHref('test"test')).toContain('"')
|
||||
})
|
||||
|
||||
it('should handle URLs with query parameters', () => {
|
||||
const url = 'https://example.com?param=<script>'
|
||||
const purified = getPurifyHref(url)
|
||||
expect(purified).not.toContain('<script>')
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(getPurifyHref('')).toBe('')
|
||||
})
|
||||
|
||||
it('should handle null/undefined', () => {
|
||||
expect(getPurifyHref(null as any)).toBe('')
|
||||
expect(getPurifyHref(undefined as any)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchWithRetry extended', () => {
|
||||
it('should succeed on first try', async () => {
|
||||
const [error, result] = await fetchWithRetry(Promise.resolve('success'))
|
||||
expect(error).toBeNull()
|
||||
expect(result).toBe('success')
|
||||
})
|
||||
|
||||
it('should retry specified number of times', async () => {
|
||||
let attempts = 0
|
||||
const failingPromise = () => {
|
||||
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
|
||||
})
|
||||
|
||||
it('should succeed after retries', async () => {
|
||||
let attempts = 0
|
||||
const eventuallySucceed = new Promise((resolve, reject) => {
|
||||
attempts++
|
||||
if (attempts < 2)
|
||||
reject(new Error('not yet'))
|
||||
else
|
||||
resolve('success')
|
||||
})
|
||||
|
||||
await fetchWithRetry(eventuallySucceed, 3)
|
||||
// Note: This test may need adjustment based on actual retry logic
|
||||
})
|
||||
|
||||
/*
|
||||
TODO: Commented this case because of eslint
|
||||
Error msg: Expected the Promise rejection reason to be an Error
|
||||
*/
|
||||
// it('should handle non-Error rejections', async () => {
|
||||
// const [error] = await fetchWithRetry(Promise.reject('string error'), 0)
|
||||
// expect(error).toBeInstanceOf(Error)
|
||||
// })
|
||||
})
|
||||
|
||||
describe('correctModelProvider extended', () => {
|
||||
it('should handle empty string', () => {
|
||||
expect(correctModelProvider('')).toBe('')
|
||||
})
|
||||
|
||||
it('should not modify provider with slash', () => {
|
||||
expect(correctModelProvider('custom/provider/model')).toBe('custom/provider/model')
|
||||
})
|
||||
|
||||
it('should handle google provider', () => {
|
||||
expect(correctModelProvider('google')).toBe('langgenius/gemini/google')
|
||||
})
|
||||
|
||||
it('should handle standard providers', () => {
|
||||
expect(correctModelProvider('openai')).toBe('langgenius/openai/openai')
|
||||
expect(correctModelProvider('anthropic')).toBe('langgenius/anthropic/anthropic')
|
||||
})
|
||||
|
||||
it('should handle null/undefined', () => {
|
||||
expect(correctModelProvider(null as any)).toBe('')
|
||||
expect(correctModelProvider(undefined as any)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('correctToolProvider extended', () => {
|
||||
it('should return as-is when toolInCollectionList is true', () => {
|
||||
expect(correctToolProvider('any-provider', true)).toBe('any-provider')
|
||||
expect(correctToolProvider('', true)).toBe('')
|
||||
})
|
||||
|
||||
it('should not modify provider with slash when not in collection', () => {
|
||||
expect(correctToolProvider('custom/tool/provider', false)).toBe('custom/tool/provider')
|
||||
})
|
||||
|
||||
it('should handle special tool providers', () => {
|
||||
expect(correctToolProvider('stepfun', false)).toBe('langgenius/stepfun_tool/stepfun')
|
||||
expect(correctToolProvider('jina', false)).toBe('langgenius/jina_tool/jina')
|
||||
expect(correctToolProvider('siliconflow', false)).toBe('langgenius/siliconflow_tool/siliconflow')
|
||||
expect(correctToolProvider('gitee_ai', false)).toBe('langgenius/gitee_ai_tool/gitee_ai')
|
||||
})
|
||||
|
||||
it('should handle standard tool providers', () => {
|
||||
expect(correctToolProvider('standard', false)).toBe('langgenius/standard/standard')
|
||||
})
|
||||
})
|
||||
|
||||
describe('canFindTool extended', () => {
|
||||
it('should match exact provider ID', () => {
|
||||
expect(canFindTool('openai', 'openai')).toBe(true)
|
||||
})
|
||||
|
||||
it('should match langgenius format', () => {
|
||||
expect(canFindTool('langgenius/openai/openai', 'openai')).toBe(true)
|
||||
})
|
||||
|
||||
it('should match tool format', () => {
|
||||
expect(canFindTool('langgenius/jina_tool/jina', 'jina')).toBe(true)
|
||||
})
|
||||
|
||||
it('should not match different providers', () => {
|
||||
expect(canFindTool('openai', 'anthropic')).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle undefined oldToolId', () => {
|
||||
expect(canFindTool('openai', undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeSpecificQueryParam extended', () => {
|
||||
beforeEach(() => {
|
||||
// Reset window.location
|
||||
delete (window as any).location
|
||||
window.location = {
|
||||
href: 'https://example.com?param1=value1¶m2=value2¶m3=value3',
|
||||
} as any
|
||||
})
|
||||
|
||||
it('should remove single query parameter', () => {
|
||||
const mockReplaceState = jest.fn()
|
||||
window.history.replaceState = mockReplaceState
|
||||
|
||||
removeSpecificQueryParam('param1')
|
||||
|
||||
expect(mockReplaceState).toHaveBeenCalled()
|
||||
const newUrl = mockReplaceState.mock.calls[0][2]
|
||||
expect(newUrl).not.toContain('param1')
|
||||
})
|
||||
|
||||
it('should remove multiple query parameters', () => {
|
||||
const mockReplaceState = jest.fn()
|
||||
window.history.replaceState = mockReplaceState
|
||||
|
||||
removeSpecificQueryParam(['param1', 'param2'])
|
||||
|
||||
expect(mockReplaceState).toHaveBeenCalled()
|
||||
const newUrl = mockReplaceState.mock.calls[0][2]
|
||||
expect(newUrl).not.toContain('param1')
|
||||
expect(newUrl).not.toContain('param2')
|
||||
})
|
||||
|
||||
it('should preserve other parameters', () => {
|
||||
const mockReplaceState = jest.fn()
|
||||
window.history.replaceState = mockReplaceState
|
||||
|
||||
removeSpecificQueryParam('param1')
|
||||
|
||||
const newUrl = mockReplaceState.mock.calls[0][2]
|
||||
expect(newUrl).toContain('param2')
|
||||
expect(newUrl).toContain('param3')
|
||||
})
|
||||
})
|
||||
|
||||
49
web/utils/urlValidation.spec.ts
Normal file
49
web/utils/urlValidation.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { validateRedirectUrl } from './urlValidation'
|
||||
|
||||
describe('URL Validation', () => {
|
||||
describe('validateRedirectUrl', () => {
|
||||
it('should reject data: protocol', () => {
|
||||
expect(() => validateRedirectUrl('data:text/html,<script>alert(1)</script>')).toThrow('Authorization URL must be HTTP or HTTPS')
|
||||
})
|
||||
|
||||
it('should reject file: protocol', () => {
|
||||
expect(() => validateRedirectUrl('file:///etc/passwd')).toThrow('Authorization URL must be HTTP or HTTPS')
|
||||
})
|
||||
|
||||
it('should reject ftp: protocol', () => {
|
||||
expect(() => validateRedirectUrl('ftp://example.com')).toThrow('Authorization URL must be HTTP or HTTPS')
|
||||
})
|
||||
|
||||
it('should reject vbscript: protocol', () => {
|
||||
expect(() => validateRedirectUrl('vbscript:msgbox(1)')).toThrow('Authorization URL must be HTTP or HTTPS')
|
||||
})
|
||||
|
||||
it('should reject malformed URLs', () => {
|
||||
expect(() => validateRedirectUrl('not a url')).toThrow('Invalid URL')
|
||||
expect(() => validateRedirectUrl('://example.com')).toThrow('Invalid URL')
|
||||
expect(() => validateRedirectUrl('')).toThrow('Invalid URL')
|
||||
})
|
||||
|
||||
it('should handle URLs with query parameters', () => {
|
||||
expect(() => validateRedirectUrl('https://example.com?param=value')).not.toThrow()
|
||||
expect(() => validateRedirectUrl('https://example.com?redirect=http://evil.com')).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle URLs with fragments', () => {
|
||||
expect(() => validateRedirectUrl('https://example.com#section')).not.toThrow()
|
||||
expect(() => validateRedirectUrl('https://example.com/path#fragment')).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle URLs with authentication', () => {
|
||||
expect(() => validateRedirectUrl('https://user:pass@example.com')).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle international domain names', () => {
|
||||
expect(() => validateRedirectUrl('https://例え.jp')).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject protocol-relative URLs', () => {
|
||||
expect(() => validateRedirectUrl('//example.com')).toThrow('Invalid URL')
|
||||
})
|
||||
})
|
||||
})
|
||||
139
web/utils/validators.spec.ts
Normal file
139
web/utils/validators.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { draft07Validator, forbidBooleanProperties } from './validators'
|
||||
|
||||
describe('Validators', () => {
|
||||
describe('draft07Validator', () => {
|
||||
it('should validate a valid JSON schema', () => {
|
||||
const validSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const result = draft07Validator(validSchema)
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should invalidate schema with unknown type', () => {
|
||||
const invalidSchema = {
|
||||
type: 'invalid_type',
|
||||
}
|
||||
const result = draft07Validator(invalidSchema)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should validate nested schemas', () => {
|
||||
const nestedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const result = draft07Validator(nestedSchema)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('should validate array schemas', () => {
|
||||
const arraySchema = {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
}
|
||||
const result = draft07Validator(arraySchema)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('forbidBooleanProperties', () => {
|
||||
it('should return empty array for schema without boolean properties', () => {
|
||||
const schema = {
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const errors = forbidBooleanProperties(schema)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should detect boolean property at root level', () => {
|
||||
const schema = {
|
||||
properties: {
|
||||
name: true,
|
||||
age: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const errors = forbidBooleanProperties(schema)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0]).toContain('name')
|
||||
})
|
||||
|
||||
it('should detect boolean properties in nested objects', () => {
|
||||
const schema = {
|
||||
properties: {
|
||||
user: {
|
||||
properties: {
|
||||
name: true,
|
||||
profile: {
|
||||
properties: {
|
||||
bio: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const errors = forbidBooleanProperties(schema)
|
||||
expect(errors).toHaveLength(2)
|
||||
expect(errors.some(e => e.includes('user.name'))).toBe(true)
|
||||
expect(errors.some(e => e.includes('user.profile.bio'))).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle schema without properties', () => {
|
||||
const schema = { type: 'string' }
|
||||
const errors = forbidBooleanProperties(schema)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle null schema', () => {
|
||||
const errors = forbidBooleanProperties(null)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle empty schema', () => {
|
||||
const errors = forbidBooleanProperties({})
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should provide correct path in error messages', () => {
|
||||
const schema = {
|
||||
properties: {
|
||||
level1: {
|
||||
properties: {
|
||||
level2: {
|
||||
properties: {
|
||||
level3: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const errors = forbidBooleanProperties(schema)
|
||||
expect(errors[0]).toContain('level1.level2.level3')
|
||||
})
|
||||
})
|
||||
})
|
||||
236
web/utils/var.spec.ts
Normal file
236
web/utils/var.spec.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
checkKey,
|
||||
checkKeys,
|
||||
getMarketplaceUrl,
|
||||
getNewVar,
|
||||
getNewVarInWorkflow,
|
||||
getVars,
|
||||
hasDuplicateStr,
|
||||
replaceSpaceWithUnderscoreInVarNameInput,
|
||||
} from './var'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
describe('Variable Utilities', () => {
|
||||
describe('checkKey', () => {
|
||||
it('should return error for empty key when canBeEmpty is false', () => {
|
||||
expect(checkKey('', false)).toBe('canNoBeEmpty')
|
||||
})
|
||||
|
||||
it('should return true for empty key when canBeEmpty is true', () => {
|
||||
expect(checkKey('', true)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return error for key that is too long', () => {
|
||||
const longKey = 'a'.repeat(101) // Assuming MAX_VAR_KEY_LENGTH is 100
|
||||
expect(checkKey(longKey)).toBe('tooLong')
|
||||
})
|
||||
|
||||
it('should return error for key starting with number', () => {
|
||||
expect(checkKey('1variable')).toBe('notStartWithNumber')
|
||||
})
|
||||
|
||||
it('should return true for valid key', () => {
|
||||
expect(checkKey('valid_variable_name')).toBe(true)
|
||||
expect(checkKey('validVariableName')).toBe(true)
|
||||
expect(checkKey('valid123')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return error for invalid characters', () => {
|
||||
expect(checkKey('invalid-key')).toBe('notValid')
|
||||
expect(checkKey('invalid key')).toBe('notValid')
|
||||
expect(checkKey('invalid.key')).toBe('notValid')
|
||||
expect(checkKey('invalid@key')).toBe('notValid')
|
||||
})
|
||||
|
||||
it('should handle underscore correctly', () => {
|
||||
expect(checkKey('_valid')).toBe(true)
|
||||
expect(checkKey('valid_name')).toBe(true)
|
||||
expect(checkKey('valid_name_123')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkKeys', () => {
|
||||
it('should return valid for all valid keys', () => {
|
||||
const result = checkKeys(['key1', 'key2', 'validKey'])
|
||||
expect(result.isValid).toBe(true)
|
||||
expect(result.errorKey).toBe('')
|
||||
expect(result.errorMessageKey).toBe('')
|
||||
})
|
||||
|
||||
it('should return error for first invalid key', () => {
|
||||
const result = checkKeys(['validKey', '1invalid', 'anotherValid'])
|
||||
expect(result.isValid).toBe(false)
|
||||
expect(result.errorKey).toBe('1invalid')
|
||||
expect(result.errorMessageKey).toBe('notStartWithNumber')
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const result = checkKeys([])
|
||||
expect(result.isValid).toBe(true)
|
||||
})
|
||||
|
||||
it('should stop checking after first error', () => {
|
||||
const result = checkKeys(['valid', 'invalid-key', '1invalid'])
|
||||
expect(result.isValid).toBe(false)
|
||||
expect(result.errorKey).toBe('invalid-key')
|
||||
expect(result.errorMessageKey).toBe('notValid')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasDuplicateStr', () => {
|
||||
it('should return false for unique strings', () => {
|
||||
expect(hasDuplicateStr(['a', 'b', 'c'])).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for duplicate strings', () => {
|
||||
expect(hasDuplicateStr(['a', 'b', 'a'])).toBe(true)
|
||||
expect(hasDuplicateStr(['test', 'test'])).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
expect(hasDuplicateStr([])).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle single element', () => {
|
||||
expect(hasDuplicateStr(['single'])).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle multiple duplicates', () => {
|
||||
expect(hasDuplicateStr(['a', 'b', 'a', 'b', 'c'])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getVars', () => {
|
||||
it('should extract variables from template string', () => {
|
||||
const result = getVars('Hello {{name}}, your age is {{age}}')
|
||||
expect(result).toEqual(['name', 'age'])
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(getVars('')).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle string without variables', () => {
|
||||
expect(getVars('Hello world')).toEqual([])
|
||||
})
|
||||
|
||||
it('should remove duplicate variables', () => {
|
||||
const result = getVars('{{name}} and {{name}} again')
|
||||
expect(result).toEqual(['name'])
|
||||
})
|
||||
|
||||
it('should filter out placeholder variables', () => {
|
||||
const result = getVars('{{#context#}} {{name}} {{#histories#}}')
|
||||
expect(result).toEqual(['name'])
|
||||
})
|
||||
|
||||
it('should handle variables with underscores', () => {
|
||||
const result = getVars('{{user_name}} {{user_age}}')
|
||||
expect(result).toEqual(['user_name', 'user_age'])
|
||||
})
|
||||
|
||||
it('should handle variables with numbers', () => {
|
||||
const result = getVars('{{var1}} {{var2}} {{var123}}')
|
||||
expect(result).toEqual(['var1', 'var2', 'var123'])
|
||||
})
|
||||
|
||||
it('should ignore invalid variable names', () => {
|
||||
const result = getVars('{{1invalid}} {{valid}} {{-invalid}}')
|
||||
expect(result).toEqual(['valid'])
|
||||
})
|
||||
|
||||
it('should filter out variables that are too long', () => {
|
||||
const longVar = 'a'.repeat(101)
|
||||
const result = getVars(`{{${longVar}}} {{valid}}`)
|
||||
expect(result).toEqual(['valid'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNewVar', () => {
|
||||
it('should create new string variable', () => {
|
||||
const result = getNewVar('testKey', 'string')
|
||||
expect(result.key).toBe('testKey')
|
||||
expect(result.type).toBe('string')
|
||||
expect(result.name).toBe('testKey')
|
||||
})
|
||||
|
||||
it('should create new number variable', () => {
|
||||
const result = getNewVar('numKey', 'number')
|
||||
expect(result.key).toBe('numKey')
|
||||
expect(result.type).toBe('number')
|
||||
})
|
||||
|
||||
it('should truncate long names', () => {
|
||||
const longKey = 'a'.repeat(100)
|
||||
const result = getNewVar(longKey, 'string')
|
||||
expect(result.name.length).toBeLessThanOrEqual(result.key.length)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNewVarInWorkflow', () => {
|
||||
it('should create text input variable by default', () => {
|
||||
const result = getNewVarInWorkflow('testVar')
|
||||
expect(result.variable).toBe('testVar')
|
||||
expect(result.type).toBe(InputVarType.textInput)
|
||||
expect(result.label).toBe('testVar')
|
||||
})
|
||||
|
||||
it('should create select variable', () => {
|
||||
const result = getNewVarInWorkflow('selectVar', InputVarType.select)
|
||||
expect(result.variable).toBe('selectVar')
|
||||
expect(result.type).toBe(InputVarType.select)
|
||||
})
|
||||
|
||||
it('should create number variable', () => {
|
||||
const result = getNewVarInWorkflow('numVar', InputVarType.number)
|
||||
expect(result.variable).toBe('numVar')
|
||||
expect(result.type).toBe(InputVarType.number)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceUrl', () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { origin: 'https://example.com' },
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should add additional parameters', () => {
|
||||
const url = getMarketplaceUrl('/plugins', { category: 'ai', version: '1.0' })
|
||||
expect(url).toContain('category=ai')
|
||||
expect(url).toContain('version=1.0')
|
||||
})
|
||||
|
||||
it('should skip undefined parameters', () => {
|
||||
const url = getMarketplaceUrl('/plugins', { category: 'ai', version: undefined })
|
||||
expect(url).toContain('category=ai')
|
||||
expect(url).not.toContain('version=')
|
||||
})
|
||||
})
|
||||
|
||||
describe('replaceSpaceWithUnderscoreInVarNameInput', () => {
|
||||
it('should replace spaces with underscores', () => {
|
||||
const input = document.createElement('input')
|
||||
input.value = 'test variable name'
|
||||
replaceSpaceWithUnderscoreInVarNameInput(input)
|
||||
expect(input.value).toBe('test_variable_name')
|
||||
})
|
||||
|
||||
it('should preserve cursor position', () => {
|
||||
const input = document.createElement('input')
|
||||
input.value = 'test name'
|
||||
input.setSelectionRange(5, 5)
|
||||
replaceSpaceWithUnderscoreInVarNameInput(input)
|
||||
expect(input.selectionStart).toBe(5)
|
||||
expect(input.selectionEnd).toBe(5)
|
||||
})
|
||||
|
||||
it('should handle multiple spaces', () => {
|
||||
const input = document.createElement('input')
|
||||
input.value = 'test multiple spaces'
|
||||
replaceSpaceWithUnderscoreInVarNameInput(input)
|
||||
expect(input.value).toBe('test__multiple___spaces')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user