mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-12 17:40:54 +08:00
【同步】前端项目源码
【修复】工作流兼容问题
This commit is contained in:
69
frontend/plugin/plugin-i18n/__tests__/adapter.test.js
Normal file
69
frontend/plugin/plugin-i18n/__tests__/adapter.test.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { TranslationAdapter } from '../src/translation/adapter/index.js'
|
||||
import { AIBatchAdapter } from '../src/translation/adapter/aiBatchAdapter.js'
|
||||
import { TraditionalApiAdapter } from '../src/translation/adapter/traditionalApiAdapter.js'
|
||||
import { ZhipuAITranslator } from '../src/translation/ai/zhipuAI.js'
|
||||
import * as api1 from '../src/translation/traditional/api1.js'
|
||||
|
||||
describe('翻译适配器测试', () => {
|
||||
describe('TranslationAdapter基类', () => {
|
||||
it('不应该允许直接实例化抽象基类', () => {
|
||||
expect(() => new TranslationAdapter()).toThrow('翻译适配器:抽象类不能被直接实例化')
|
||||
})
|
||||
})
|
||||
|
||||
describe('AI批量翻译适配器', () => {
|
||||
const adapter = new AIBatchAdapter()
|
||||
const text = '你好'
|
||||
const apiKey = 'test-key'
|
||||
const languages = ['enUS', 'jaJP']
|
||||
const maxRetries = 3
|
||||
|
||||
it('应该实现translate方法', () => {
|
||||
expect(typeof adapter.translate).toBe('function')
|
||||
})
|
||||
|
||||
it('应该正确处理翻译失败和重试机制', async () => {
|
||||
const mockTranslate = jest
|
||||
.spyOn(ZhipuAITranslator.prototype, 'translate')
|
||||
.mockRejectedValueOnce(new Error('API调用失败'))
|
||||
.mockResolvedValueOnce({
|
||||
text,
|
||||
translations: {
|
||||
enUS: 'Hello',
|
||||
jaJP: 'こんにちは',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await adapter.translate(text, apiKey, languages, maxRetries)
|
||||
expect(mockTranslate).toHaveBeenCalledTimes(2)
|
||||
expect(result.translations.enUS).toBe('Hello')
|
||||
expect(result.translations.jaJP).toBe('こんにちは')
|
||||
})
|
||||
})
|
||||
|
||||
describe('传统API翻译适配器', () => {
|
||||
const adapter = new TraditionalApiAdapter(api1)
|
||||
|
||||
it('应该验证API模块的有效性', () => {
|
||||
expect(() => new TraditionalApiAdapter()).toThrow('传统API适配器:无效的API模块')
|
||||
})
|
||||
|
||||
it('应该能够获取支持的语言列表', () => {
|
||||
const supportedLanguages = adapter.getSupportedLanguages()
|
||||
expect(Array.isArray(supportedLanguages)).toBe(true)
|
||||
expect(supportedLanguages.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('应该正确处理不支持的语言', async () => {
|
||||
const text = '你好'
|
||||
const apiKey = 'test-key'
|
||||
const languages = ['invalidLang']
|
||||
const maxRetries = 3
|
||||
|
||||
await expect(adapter.translate(text, apiKey, languages, maxRetries)).rejects.toThrow(
|
||||
'传统API适配器:不支持的目标语言',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
120
frontend/plugin/plugin-i18n/__tests__/cache.test.js
Normal file
120
frontend/plugin/plugin-i18n/__tests__/cache.test.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { CacheManager } from '../src/cache/index.js'
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
mkdir: jest.fn(),
|
||||
readFile: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
access: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('CacheManager', () => {
|
||||
const cachePath = './test-cache.json'
|
||||
let cacheManager
|
||||
|
||||
beforeEach(() => {
|
||||
cacheManager = new CacheManager(cachePath)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('initCache', () => {
|
||||
it('应该正确初始化缓存', async () => {
|
||||
const mockCacheData = {
|
||||
test: {
|
||||
text: 'test',
|
||||
translations: { enUS: 'test' },
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
}
|
||||
|
||||
fs.access.mockResolvedValueOnce()
|
||||
fs.readFile.mockResolvedValueOnce(JSON.stringify(mockCacheData))
|
||||
|
||||
await cacheManager.initCache()
|
||||
expect(cacheManager.cache.get('test')).toEqual(mockCacheData.test)
|
||||
})
|
||||
|
||||
it('处理缓存文件不存在的情况', async () => {
|
||||
fs.access.mockRejectedValueOnce(new Error('文件不存在'))
|
||||
|
||||
await cacheManager.initCache()
|
||||
expect(cacheManager.cache.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCachedTranslations', () => {
|
||||
beforeEach(async () => {
|
||||
cacheManager.cache.set('hello', {
|
||||
text: 'hello',
|
||||
translations: {
|
||||
enUS: 'Hello',
|
||||
jaJP: 'こんにちは',
|
||||
},
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
})
|
||||
})
|
||||
|
||||
it('应该返回缓存的翻译', async () => {
|
||||
const texts = ['hello', 'world']
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
const { cached, uncached } = await cacheManager.getCachedTranslations(texts, languages)
|
||||
|
||||
expect(cached.hello).toBeDefined()
|
||||
expect(uncached).toContain('world')
|
||||
})
|
||||
|
||||
it('检查缓存项是否包含所有必要的语言', async () => {
|
||||
const texts = ['hello']
|
||||
const languages = ['enUS', 'jaJP', 'zhCN']
|
||||
|
||||
const { cached, uncached } = await cacheManager.getCachedTranslations(texts, languages)
|
||||
|
||||
expect(uncached).toContain('hello')
|
||||
expect(Object.keys(cached)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateCache', () => {
|
||||
it('应该正确更新缓存', async () => {
|
||||
const texts = ['test']
|
||||
const translations = [
|
||||
{
|
||||
text: 'test',
|
||||
translations: {
|
||||
enUS: 'Test',
|
||||
jaJP: 'テスト',
|
||||
},
|
||||
},
|
||||
]
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
await cacheManager.updateCache(texts, translations, languages)
|
||||
|
||||
const cached = cacheManager.cache.get('test')
|
||||
expect(cached.translations.enUS).toBe('Test')
|
||||
expect(cached.translations.jaJP).toBe('テスト')
|
||||
expect(fs.writeFile).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanCache', () => {
|
||||
it('应该删除无效的缓存项', async () => {
|
||||
cacheManager.cache.set('valid', { text: 'valid' })
|
||||
cacheManager.cache.set('invalid', { text: 'invalid' })
|
||||
|
||||
await cacheManager.cleanCache(['valid'])
|
||||
|
||||
expect(cacheManager.cache.has('valid')).toBe(true)
|
||||
expect(cacheManager.cache.has('invalid')).toBe(false)
|
||||
expect(fs.writeFile).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
133
frontend/plugin/plugin-i18n/__tests__/log.test.js
Normal file
133
frontend/plugin/plugin-i18n/__tests__/log.test.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { LogManager } from '../src/logManagement/index.js'
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
mkdir: jest.fn(),
|
||||
appendFile: jest.fn(),
|
||||
readFile: jest.fn(),
|
||||
readdir: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('LogManager', () => {
|
||||
const options = {
|
||||
logPath: './test-logs',
|
||||
errorLogFile: 'error.log',
|
||||
infoLogFile: 'info.log',
|
||||
}
|
||||
let logManager
|
||||
|
||||
beforeEach(() => {
|
||||
logManager = new LogManager(options)
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
it('应该创建日志目录', async () => {
|
||||
await logManager.init()
|
||||
expect(fs.mkdir).toHaveBeenCalledWith(options.logPath, { recursive: true })
|
||||
})
|
||||
|
||||
it('处理创建目录失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.mkdir.mockRejectedValueOnce(new Error('创建目录失败'))
|
||||
|
||||
await logManager.init()
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('logError', () => {
|
||||
it('应该写入错误日志', async () => {
|
||||
const error = new Error('测试错误')
|
||||
await logManager.logError(error)
|
||||
|
||||
expect(fs.appendFile).toHaveBeenCalledWith(
|
||||
expect.stringContaining('error.log'),
|
||||
expect.stringContaining('测试错误'),
|
||||
)
|
||||
})
|
||||
|
||||
it('处理写入错误日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.appendFile.mockRejectedValueOnce(new Error('写入失败'))
|
||||
|
||||
await logManager.logError(new Error('测试错误'))
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('logInfo', () => {
|
||||
it('应该写入信息日志', async () => {
|
||||
const message = '测试信息'
|
||||
await logManager.logInfo(message)
|
||||
|
||||
expect(fs.appendFile).toHaveBeenCalledWith(expect.stringContaining('info.log'), expect.stringContaining(message))
|
||||
})
|
||||
|
||||
it('处理写入信息日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.appendFile.mockRejectedValueOnce(new Error('写入失败'))
|
||||
|
||||
await logManager.logInfo('测试信息')
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanLogs', () => {
|
||||
it('应该删除过期的日志文件', async () => {
|
||||
const now = Date.now()
|
||||
const oldDate = now - 7 * 24 * 60 * 60 * 1000 - 1000 // 7天+1秒前
|
||||
|
||||
fs.readdir.mockResolvedValueOnce(['old.log', 'new.log'])
|
||||
fs.stat.mockImplementation((path) => {
|
||||
return Promise.resolve({
|
||||
mtimeMs: path.includes('old') ? oldDate : now,
|
||||
})
|
||||
})
|
||||
|
||||
await logManager.cleanLogs(7)
|
||||
|
||||
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining('old.log'))
|
||||
expect(fs.unlink).not.toHaveBeenCalledWith(expect.stringContaining('new.log'))
|
||||
})
|
||||
|
||||
it('处理清理日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.readdir.mockRejectedValueOnce(new Error('读取目录失败'))
|
||||
|
||||
await logManager.cleanLogs(7)
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLogs', () => {
|
||||
it('应该返回指定数量的日志行', async () => {
|
||||
const logContent = '行1\n行2\n行3\n行4\n行5'
|
||||
fs.readFile.mockResolvedValueOnce(logContent)
|
||||
|
||||
const lines = await logManager.getLogs('info', 3)
|
||||
expect(lines).toHaveLength(3)
|
||||
expect(lines[2]).toBe('行5')
|
||||
})
|
||||
|
||||
it('处理读取日志失败的情况', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
||||
fs.readFile.mockRejectedValueOnce(new Error('读取文件失败'))
|
||||
|
||||
const lines = await logManager.getLogs('error', 5)
|
||||
expect(lines).toHaveLength(0)
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
100
frontend/plugin/plugin-i18n/__tests__/utils.extend.test.js
Normal file
100
frontend/plugin/plugin-i18n/__tests__/utils.extend.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Utils } from '../src/utils/index.js'
|
||||
|
||||
describe('Utils Extended Features', () => {
|
||||
describe('chunkArray', () => {
|
||||
it('应该正确分块数组', () => {
|
||||
const array = [1, 2, 3, 4, 5, 6, 7]
|
||||
const size = 3
|
||||
const chunks = Utils.chunkArray(array, size)
|
||||
|
||||
expect(chunks).toHaveLength(3)
|
||||
expect(chunks[0]).toEqual([1, 2, 3])
|
||||
expect(chunks[1]).toEqual([4, 5, 6])
|
||||
expect(chunks[2]).toEqual([7])
|
||||
})
|
||||
|
||||
it('处理空数组', () => {
|
||||
const chunks = Utils.chunkArray([], 2)
|
||||
expect(chunks).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delay', () => {
|
||||
it('应该延迟执行指定时间', async () => {
|
||||
const start = Date.now()
|
||||
await Utils.delay(100)
|
||||
const duration = Date.now() - start
|
||||
|
||||
expect(duration).toBeGreaterThanOrEqual(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractChineseTexts', () => {
|
||||
it('应该正确提取中文内容', () => {
|
||||
const content = `
|
||||
$t('你好世界')
|
||||
$t("测试文本")
|
||||
$t('Hello World')
|
||||
`
|
||||
const templateRegex = /\$t\(['"]([^'"]+)['"]\)/g
|
||||
|
||||
const texts = Utils.extractChineseTexts(content, templateRegex)
|
||||
expect(texts.size).toBe(2)
|
||||
expect(texts.has('你好世界')).toBe(true)
|
||||
expect(texts.has('测试文本')).toBe(true)
|
||||
expect(texts.has('Hello World')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergeTranslations', () => {
|
||||
it('应该正确合并翻译结果', () => {
|
||||
const target = {
|
||||
key1: 'old value 1',
|
||||
key2: 'old value 2',
|
||||
}
|
||||
const source = {
|
||||
key1: 'new value 1',
|
||||
key3: 'new value 3',
|
||||
}
|
||||
|
||||
const result = Utils.mergeTranslations(target, source)
|
||||
expect(result).toEqual({
|
||||
key1: 'new value 1',
|
||||
key2: 'old value 2',
|
||||
key3: 'new value 3',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidLanguageCode', () => {
|
||||
it('应该验证语言代码格式', () => {
|
||||
expect(Utils.isValidLanguageCode('zhCN')).toBe(true)
|
||||
expect(Utils.isValidLanguageCode('enUS')).toBe(true)
|
||||
expect(Utils.isValidLanguageCode('zh-CN')).toBe(false)
|
||||
expect(Utils.isValidLanguageCode('123')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatError', () => {
|
||||
it('应该正确格式化错误信息', () => {
|
||||
const error = new Error('测试错误')
|
||||
const formatted = Utils.formatError(error)
|
||||
|
||||
expect(formatted).toHaveProperty('message', '测试错误')
|
||||
expect(formatted).toHaveProperty('stack')
|
||||
expect(formatted).toHaveProperty('timestamp')
|
||||
expect(new Date(formatted.timestamp)).toBeInstanceOf(Date)
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateId', () => {
|
||||
it('应该生成唯一的标识符', () => {
|
||||
const id1 = Utils.generateId()
|
||||
const id2 = Utils.generateId()
|
||||
|
||||
expect(id1).toMatch(/^translation_\d+$/)
|
||||
expect(id2).toMatch(/^translation_\d+$/)
|
||||
expect(id1).not.toBe(id2)
|
||||
})
|
||||
})
|
||||
})
|
||||
62
frontend/plugin/plugin-i18n/__tests__/utils.test.js
Normal file
62
frontend/plugin/plugin-i18n/__tests__/utils.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Utils } from '../src/utils/index.js'
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('isChineseText', () => {
|
||||
it('应该正确识别中文文本', () => {
|
||||
expect(Utils.isChineseText('你好')).toBe(true)
|
||||
expect(Utils.isChineseText('Hello')).toBe(false)
|
||||
expect(Utils.isChineseText('Hello 你好')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateConfig', () => {
|
||||
it('应该验证配置对象', () => {
|
||||
const validConfig = {
|
||||
apiKey: { zhipuAI: 'test-key' },
|
||||
languages: ['zhCN', 'enUS'],
|
||||
concurrency: 10,
|
||||
interval: 1000,
|
||||
}
|
||||
|
||||
const errors = Utils.validateConfig(validConfig)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('应该检测无效的配置', () => {
|
||||
const invalidConfig = {
|
||||
apiKey: 'invalid',
|
||||
languages: ['invalid'],
|
||||
concurrency: -1,
|
||||
interval: 'invalid',
|
||||
}
|
||||
|
||||
const errors = Utils.validateConfig(invalidConfig)
|
||||
expect(errors.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseLanguageCode', () => {
|
||||
it('应该正确解析语言代码', () => {
|
||||
const result = Utils.parseLanguageCode('zhCN')
|
||||
expect(result).toEqual({
|
||||
language: 'zh',
|
||||
region: 'CN',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatTranslations', () => {
|
||||
it('应该正确格式化翻译结果', () => {
|
||||
const translations = {
|
||||
hello: ' Hello World ',
|
||||
welcome: ' 欢迎 ',
|
||||
}
|
||||
|
||||
const formatted = Utils.formatTranslations(translations)
|
||||
expect(formatted).toEqual({
|
||||
hello: 'Hello World',
|
||||
welcome: '欢迎',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
112
frontend/plugin/plugin-i18n/__tests__/zhipuAI.test.js
Normal file
112
frontend/plugin/plugin-i18n/__tests__/zhipuAI.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import axios from 'axios'
|
||||
import { ZhipuAITranslator } from '../src/translation/ai/zhipuAI.js'
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
describe('ZhipuAITranslator', () => {
|
||||
const apiKey = 'test-key'
|
||||
let translator
|
||||
|
||||
beforeEach(() => {
|
||||
translator = new ZhipuAITranslator(apiKey)
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('generatePrompt', () => {
|
||||
it('应该生成正确的提示词', () => {
|
||||
const text = '你好'
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
const prompt = translator.generatePrompt(text, languages)
|
||||
expect(prompt).toContain('请将以下文本翻译成')
|
||||
expect(prompt).toContain('en-US, ja-JP')
|
||||
expect(prompt).toContain(text)
|
||||
})
|
||||
})
|
||||
|
||||
describe('translate', () => {
|
||||
const text = '你好'
|
||||
const languages = ['enUS', 'jaJP']
|
||||
|
||||
it('应该成功调用API并返回翻译结果', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: JSON.stringify({
|
||||
'en-US': 'Hello',
|
||||
'ja-JP': 'こんにちは',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse)
|
||||
|
||||
const result = await translator.translate(text, languages)
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/chatglm_turbo'),
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
text: '你好',
|
||||
translations: {
|
||||
enUS: 'Hello',
|
||||
jaJP: 'こんにちは',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('处理API调用失败的情况', async () => {
|
||||
axios.post.mockRejectedValueOnce(new Error('API调用失败'))
|
||||
|
||||
await expect(translator.translate(text, languages)).rejects.toThrow('智谱AI翻译失败')
|
||||
})
|
||||
|
||||
it('处理无效的API响应', async () => {
|
||||
const mockResponse = {
|
||||
data: {},
|
||||
}
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse)
|
||||
|
||||
await expect(translator.translate(text, languages)).rejects.toThrow('无效的API响应')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateApiKey', () => {
|
||||
it('应该成功验证有效的API密钥', async () => {
|
||||
axios.get.mockResolvedValueOnce({})
|
||||
|
||||
const isValid = await translator.validateApiKey()
|
||||
expect(isValid).toBe(true)
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/validate'),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('处理无效的API密钥', async () => {
|
||||
axios.get.mockRejectedValueOnce(new Error('无效的密钥'))
|
||||
|
||||
const isValid = await translator.validateApiKey()
|
||||
expect(isValid).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user