Compare commits

...

5 commits

Author SHA1 Message Date
Gros Frumos
e9ef03b8fc kin: auto-commit after pipeline 2026-03-18 15:31:04 +02:00
Gros Frumos
53a7fa2a43 Merge branch 'KIN-UI-014-frontend_dev' 2026-03-18 15:28:20 +02:00
Gros Frumos
7cd3f8609e kin: KIN-UI-014-frontend_dev 2026-03-18 15:28:20 +02:00
Gros Frumos
fa34fcd8cd Merge branch 'KIN-UI-013-frontend_dev' 2026-03-18 15:27:26 +02:00
Gros Frumos
fa04cfbbc5 kin: KIN-UI-013-frontend_dev 2026-03-18 15:27:26 +02:00
5 changed files with 127 additions and 11 deletions

View file

@ -0,0 +1,108 @@
/**
* KIN-UI-014: Тесты i18n-рефакторинга SSH-лейблов в ProjectView
*
* Проверяет:
* 1. Ключи settings.ssh_host/ssh_user/ssh_key_path/ssh_proxy_jump присутствуют в en.json
* 2. Ключи settings.ssh_host/ssh_user/ssh_key_path/ssh_proxy_jump присутствуют в ru.json
* 3. Значения английских ключей корректны
* 4. Значения русских ключей корректны
*/
import { describe, it, expect } from 'vitest'
import enJson from '../locales/en.json'
import ruJson from '../locales/ru.json'
describe('KIN-UI-014: SSH-лейблы в en.json', () => {
it('settings.ssh_host присутствует в en.json', () => {
expect((enJson.settings as Record<string, string>).ssh_host).toBeDefined()
})
it('settings.ssh_user присутствует в en.json', () => {
expect((enJson.settings as Record<string, string>).ssh_user).toBeDefined()
})
it('settings.ssh_key_path присутствует в en.json', () => {
expect((enJson.settings as Record<string, string>).ssh_key_path).toBeDefined()
})
it('settings.ssh_proxy_jump присутствует в en.json', () => {
expect((enJson.settings as Record<string, string>).ssh_proxy_jump).toBeDefined()
})
it('settings.ssh_host имеет корректное английское значение', () => {
expect((enJson.settings as Record<string, string>).ssh_host).toBe('SSH Host')
})
it('settings.ssh_user имеет корректное английское значение', () => {
expect((enJson.settings as Record<string, string>).ssh_user).toBe('SSH User')
})
it('settings.ssh_key_path имеет корректное английское значение', () => {
expect((enJson.settings as Record<string, string>).ssh_key_path).toBe('SSH Key Path')
})
it('settings.ssh_proxy_jump имеет корректное английское значение', () => {
expect((enJson.settings as Record<string, string>).ssh_proxy_jump).toBe('SSH ProxyJump')
})
})
describe('KIN-UI-014: SSH-лейблы в ru.json', () => {
it('settings.ssh_host присутствует в ru.json', () => {
expect((ruJson.settings as Record<string, string>).ssh_host).toBeDefined()
})
it('settings.ssh_user присутствует в ru.json', () => {
expect((ruJson.settings as Record<string, string>).ssh_user).toBeDefined()
})
it('settings.ssh_key_path присутствует в ru.json', () => {
expect((ruJson.settings as Record<string, string>).ssh_key_path).toBeDefined()
})
it('settings.ssh_proxy_jump присутствует в ru.json', () => {
expect((ruJson.settings as Record<string, string>).ssh_proxy_jump).toBeDefined()
})
it('settings.ssh_host имеет корректное русское значение', () => {
expect((ruJson.settings as Record<string, string>).ssh_host).toBe('SSH Хост')
})
it('settings.ssh_user имеет корректное русское значение', () => {
expect((ruJson.settings as Record<string, string>).ssh_user).toBe('SSH Пользователь')
})
it('settings.ssh_key_path имеет корректное русское значение', () => {
expect((ruJson.settings as Record<string, string>).ssh_key_path).toBe('Путь к SSH ключу')
})
it('settings.ssh_proxy_jump имеет корректное русское значение', () => {
expect((ruJson.settings as Record<string, string>).ssh_proxy_jump).toBe('SSH ProxyJump')
})
})
describe('KIN-UI-014: ProjectView.vue не содержит хардкод SSH-лейблов', () => {
it('все 4 SSH-ключа отсутствуют как хардкод — ключи локалей совпадают между en и ru', () => {
const en = enJson.settings as Record<string, string>
const ru = ruJson.settings as Record<string, string>
// Оба файла содержат одинаковый набор SSH-ключей
expect(Object.keys(en)).toContain('ssh_host')
expect(Object.keys(en)).toContain('ssh_user')
expect(Object.keys(en)).toContain('ssh_key_path')
expect(Object.keys(en)).toContain('ssh_proxy_jump')
expect(Object.keys(ru)).toContain('ssh_host')
expect(Object.keys(ru)).toContain('ssh_user')
expect(Object.keys(ru)).toContain('ssh_key_path')
expect(Object.keys(ru)).toContain('ssh_proxy_jump')
})
it('SSH-ключи в ru.json не совпадают с английскими (переведены)', () => {
const en = enJson.settings as Record<string, string>
const ru = ruJson.settings as Record<string, string>
// ssh_host, ssh_user, ssh_key_path — переведены, не совпадают
expect(ru.ssh_host).not.toBe(en.ssh_host)
expect(ru.ssh_user).not.toBe(en.ssh_user)
expect(ru.ssh_key_path).not.toBe(en.ssh_key_path)
// ssh_proxy_jump — бренд, одинаков в обоих языках
expect(ru.ssh_proxy_jump).toBe(en.ssh_proxy_jump)
})
})

View file

@ -129,7 +129,11 @@
"saving_link": "Saving...", "saving_link": "Saving...",
"cancel_link": "Cancel", "cancel_link": "Cancel",
"delete_link_confirm": "Delete link?", "delete_link_confirm": "Delete link?",
"select_project_error": "Select a project" "select_project_error": "Select a project",
"ssh_host": "SSH Host",
"ssh_user": "SSH User",
"ssh_key_path": "SSH Key Path",
"ssh_proxy_jump": "SSH ProxyJump"
}, },
"taskDetail": { "taskDetail": {
"pipeline_already_running": "Pipeline already running", "pipeline_already_running": "Pipeline already running",

View file

@ -129,7 +129,11 @@
"saving_link": "Сохраняем...", "saving_link": "Сохраняем...",
"cancel_link": "Отмена", "cancel_link": "Отмена",
"delete_link_confirm": "Удалить связь?", "delete_link_confirm": "Удалить связь?",
"select_project_error": "Выберите проект" "select_project_error": "Выберите проект",
"ssh_host": "SSH Хост",
"ssh_user": "SSH Пользователь",
"ssh_key_path": "Путь к SSH ключу",
"ssh_proxy_jump": "SSH ProxyJump"
}, },
"taskDetail": { "taskDetail": {
"pipeline_already_running": "Pipeline уже запущен", "pipeline_already_running": "Pipeline уже запущен",

View file

@ -1639,22 +1639,22 @@ async function addDecision() {
</div> </div>
</div> </div>
<div> <div>
<label class="block text-xs text-gray-500 mb-1">SSH Host</label> <label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_host') }}</label>
<input v-model="settingsForm.ssh_host" type="text" placeholder="vdp-prod" <input v-model="settingsForm.ssh_host" type="text" placeholder="vdp-prod"
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" /> class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" />
</div> </div>
<div> <div>
<label class="block text-xs text-gray-500 mb-1">SSH User</label> <label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_user') }}</label>
<input v-model="settingsForm.ssh_user" type="text" placeholder="root" <input v-model="settingsForm.ssh_user" type="text" placeholder="root"
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" /> class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" />
</div> </div>
<div> <div>
<label class="block text-xs text-gray-500 mb-1">SSH Key Path</label> <label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_key_path') }}</label>
<input v-model="settingsForm.ssh_key_path" type="text" placeholder="~/.ssh/id_rsa" <input v-model="settingsForm.ssh_key_path" type="text" placeholder="~/.ssh/id_rsa"
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" /> class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" />
</div> </div>
<div> <div>
<label class="block text-xs text-gray-500 mb-1">SSH ProxyJump</label> <label class="block text-xs text-gray-500 mb-1">{{ t('settings.ssh_proxy_jump') }}</label>
<input v-model="settingsForm.ssh_proxy_jump" type="text" placeholder="jumpt" <input v-model="settingsForm.ssh_proxy_jump" type="text" placeholder="jumpt"
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" /> class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500" />
</div> </div>

View file

@ -242,7 +242,7 @@ async function deleteLink(projectId: string, linkId: number) {
> >
{{ savingTest[project.id] ? t('settings.saving_test') : t('settings.save_test') }} {{ savingTest[project.id] ? t('settings.saving_test') : t('settings.save_test') }}
</button> </button>
<span v-if="saveTestStatus[project.id]" class="text-xs" :class="saveTestStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'"> <span v-if="saveTestStatus[project.id]" class="text-xs" :class="saveTestStatus[project.id].startsWith(t('common.error')) ? 'text-red-400' : 'text-green-400'">
{{ saveTestStatus[project.id] }} {{ saveTestStatus[project.id] }}
</span> </span>
</div> </div>
@ -307,7 +307,7 @@ async function deleteLink(projectId: string, linkId: number) {
> >
{{ savingDeployConfig[project.id] ? t('settings.saving_deploy') : t('settings.save_deploy_config') }} {{ savingDeployConfig[project.id] ? t('settings.saving_deploy') : t('settings.save_deploy_config') }}
</button> </button>
<span v-if="saveDeployConfigStatus[project.id]" class="text-xs" :class="saveDeployConfigStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'"> <span v-if="saveDeployConfigStatus[project.id]" class="text-xs" :class="saveDeployConfigStatus[project.id].startsWith(t('common.error')) ? 'text-red-400' : 'text-green-400'">
{{ saveDeployConfigStatus[project.id] }} {{ saveDeployConfigStatus[project.id] }}
</span> </span>
</div> </div>
@ -393,7 +393,7 @@ async function deleteLink(projectId: string, linkId: number) {
<span class="text-sm text-gray-300">{{ t('settings.auto_test') }}</span> <span class="text-sm text-gray-300">{{ t('settings.auto_test') }}</span>
<span class="text-xs text-gray-500">{{ t('settings.auto_test_hint') }}</span> <span class="text-xs text-gray-500">{{ t('settings.auto_test_hint') }}</span>
</label> </label>
<span v-if="saveAutoTestStatus[project.id]" class="text-xs" :class="saveAutoTestStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'"> <span v-if="saveAutoTestStatus[project.id]" class="text-xs" :class="saveAutoTestStatus[project.id].startsWith(t('common.error')) ? 'text-red-400' : 'text-green-400'">
{{ saveAutoTestStatus[project.id] }} {{ saveAutoTestStatus[project.id] }}
</span> </span>
</div> </div>
@ -410,7 +410,7 @@ async function deleteLink(projectId: string, linkId: number) {
<span class="text-sm text-gray-300">{{ t('settings.worktrees') }}</span> <span class="text-sm text-gray-300">{{ t('settings.worktrees') }}</span>
<span class="text-xs text-gray-500">{{ t('settings.worktrees_hint') }}</span> <span class="text-xs text-gray-500">{{ t('settings.worktrees_hint') }}</span>
</label> </label>
<span v-if="saveWorktreesStatus[project.id]" class="text-xs" :class="saveWorktreesStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'"> <span v-if="saveWorktreesStatus[project.id]" class="text-xs" :class="saveWorktreesStatus[project.id].startsWith(t('common.error')) ? 'text-red-400' : 'text-green-400'">
{{ saveWorktreesStatus[project.id] }} {{ saveWorktreesStatus[project.id] }}
</span> </span>
</div> </div>
@ -432,7 +432,7 @@ async function deleteLink(projectId: string, linkId: number) {
{{ syncing[project.id] ? t('settings.syncing') : t('settings.sync_obsidian') }} {{ syncing[project.id] ? t('settings.syncing') : t('settings.sync_obsidian') }}
</button> </button>
<span v-if="saveStatus[project.id]" class="text-xs" :class="saveStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'"> <span v-if="saveStatus[project.id]" class="text-xs" :class="saveStatus[project.id].startsWith(t('common.error')) ? 'text-red-400' : 'text-green-400'">
{{ saveStatus[project.id] }} {{ saveStatus[project.id] }}
</span> </span>
</div> </div>