This commit is contained in:
parent
b3132edfb7
commit
dc188b1ed8
@ -13,7 +13,7 @@
|
||||
size="small"
|
||||
plain
|
||||
:loading="loading"
|
||||
:disabled="!!working || sectionLoading || loading"
|
||||
:disabled="busy || sectionLoading || loading"
|
||||
@click="refreshPage"
|
||||
>
|
||||
刷新状态
|
||||
@ -48,13 +48,21 @@
|
||||
show-icon
|
||||
class="config-alert"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="saveError"
|
||||
:title="saveError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="config-alert"
|
||||
/>
|
||||
|
||||
<ConfigForm
|
||||
v-if="!sectionLoading && !sectionError"
|
||||
:section="selectedSection"
|
||||
:config="formConfig"
|
||||
:errors="formErrors"
|
||||
:disabled="!canWrite || !!working || !!pendingCandidate"
|
||||
:disabled="!canWrite || busy || !!pendingCandidate"
|
||||
@change="updateField"
|
||||
/>
|
||||
<div v-if="secretField" class="secret-editor">
|
||||
@ -68,7 +76,7 @@
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
:disabled="!canWrite || sectionLoading || !!working || !!sectionError || !!pendingCandidate"
|
||||
:disabled="!canWrite || sectionLoading || busy || !!sectionError || !!pendingCandidate"
|
||||
:placeholder="secretField.placeholder"
|
||||
autocomplete="new-password"
|
||||
@update:model-value="updateSecret"
|
||||
@ -77,14 +85,14 @@
|
||||
|
||||
<div v-if="canWrite" class="editor-footer">
|
||||
<el-alert
|
||||
v-if="pendingCandidate"
|
||||
v-if="showPendingRecovery"
|
||||
title="上次保存尚未完成,可以继续保存或放弃本次更改。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-if="pendingScopeHint"
|
||||
v-if="showPendingRecovery && pendingScopeHint"
|
||||
:title="pendingScopeHint"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
@ -92,28 +100,18 @@
|
||||
/>
|
||||
<div class="action-row">
|
||||
<el-button
|
||||
v-if="!pendingCandidate"
|
||||
type="primary"
|
||||
:loading="working === 'save' || working === 'apply'"
|
||||
:disabled="sectionLoading || !!working || !!sectionError || !hasChanges"
|
||||
:loading="busy"
|
||||
:disabled="sectionLoading || busy || !!sectionError || (!pendingCandidate && !hasChanges)"
|
||||
@click="saveAndApply"
|
||||
>
|
||||
保存配置
|
||||
{{ continuingSave ? '继续保存' : '保存配置' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
:loading="working === 'resume' || working === 'apply'"
|
||||
:disabled="sectionLoading || !!working || !!sectionError"
|
||||
@click="continuePending"
|
||||
>
|
||||
继续保存
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="pendingCandidate"
|
||||
v-if="showPendingRecovery"
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="!!working || pendingCandidate.committed"
|
||||
:disabled="busy || pendingCandidate.committed"
|
||||
@click="managePending('cancel')"
|
||||
>
|
||||
放弃更改
|
||||
@ -141,7 +139,9 @@ import ConfigForm from '../ConfigForm/ConfigForm.vue'
|
||||
const {
|
||||
availabilityError,
|
||||
loading,
|
||||
working,
|
||||
busy,
|
||||
showPendingRecovery,
|
||||
continuingSave,
|
||||
status,
|
||||
selectedSection,
|
||||
currentSection,
|
||||
@ -151,6 +151,7 @@ const {
|
||||
updateField,
|
||||
updateSecret,
|
||||
sectionError,
|
||||
saveError,
|
||||
secretField,
|
||||
secretValue,
|
||||
pendingCandidate,
|
||||
@ -160,8 +161,7 @@ const {
|
||||
hasChanges,
|
||||
canWrite,
|
||||
refreshPage,
|
||||
saveAndApply,
|
||||
continuePending
|
||||
saveAndApply
|
||||
} = inject('smail-config-page')
|
||||
</script>
|
||||
|
||||
|
||||
@ -41,6 +41,9 @@ export function usePage(pageKey = 'base') {
|
||||
const loading = ref(false)
|
||||
const availabilityError = ref('')
|
||||
const working = ref('')
|
||||
// 一次保存跨越暂存、应用和回读;中间候选不是需要用户恢复的失败记录。
|
||||
const saveFlow = ref('')
|
||||
const busy = computed(() => !!working.value || !!saveFlow.value)
|
||||
const backupsLoading = ref(false)
|
||||
const auditLoading = ref(false)
|
||||
const backupsError = ref('')
|
||||
@ -67,10 +70,13 @@ export function usePage(pageKey = 'base') {
|
||||
const formErrors = ref({})
|
||||
const loadedText = ref('')
|
||||
const sectionError = ref('')
|
||||
const saveError = ref('')
|
||||
const secretValue = ref('')
|
||||
const confirmed = ref(false)
|
||||
const savedGeneration = ref(0)
|
||||
const pendingCandidate = ref(null)
|
||||
const showPendingRecovery = computed(() => !!pendingCandidate.value && !saveFlow.value)
|
||||
const continuingSave = computed(() => saveFlow.value === 'resume' || showPendingRecovery.value)
|
||||
const pendingScopeHint = computed(() => {
|
||||
const pending = pendingCandidate.value
|
||||
if (pending?.section === 'plugins' && apiSection() === 'plugins') {
|
||||
@ -178,16 +184,18 @@ export function usePage(pageKey = 'base') {
|
||||
const canOperate = () => !disposed && canWrite.value && !loading.value && !working.value && !sectionLoading.value && !sectionError.value && !availabilityError.value
|
||||
const canEdit = () => canOperate() && !pendingCandidate.value && !savedGeneration.value
|
||||
const updateField = (path, value) => {
|
||||
if (!canEdit()) return
|
||||
if (saveFlow.value || !canEdit()) return
|
||||
const updated = updateFormConfig(editorText.value, selectedSection.value, path, value)
|
||||
if (updated === editorText.value) return
|
||||
editorText.value = updated
|
||||
saveError.value = ''
|
||||
if (Object.keys(formErrors.value).length) formErrors.value = validateFormConfig(selectedSection.value, formConfig.value)
|
||||
invalidateCandidate()
|
||||
}
|
||||
const updateSecret = value => {
|
||||
if (!canEdit() || !secretField.value || secretValue.value === value) return
|
||||
if (saveFlow.value || !canEdit() || !secretField.value || secretValue.value === value) return
|
||||
secretValue.value = value
|
||||
saveError.value = ''
|
||||
invalidateCandidate()
|
||||
}
|
||||
|
||||
@ -246,6 +254,7 @@ export function usePage(pageKey = 'base') {
|
||||
|
||||
const managePending = async action => {
|
||||
if (!canOperate() || !pendingCandidate.value) return false
|
||||
saveError.value = ''
|
||||
working.value = action
|
||||
try {
|
||||
if (action === 'cancel') {
|
||||
@ -268,7 +277,10 @@ export function usePage(pageKey = 'base') {
|
||||
if (action === 'cancel') ElMessage.success('本次更改已放弃')
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(error.message || '未完成的配置更改处理失败')
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
saveError.value = error.message || '未完成的配置更改处理失败'
|
||||
ElMessage.error(saveError.value)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
@ -280,6 +292,7 @@ export function usePage(pageKey = 'base') {
|
||||
const section = apiSection()
|
||||
sectionLoading.value = true
|
||||
sectionError.value = ''
|
||||
saveError.value = ''
|
||||
formErrors.value = {}
|
||||
editorText.value = ''
|
||||
loadedText.value = ''
|
||||
@ -571,6 +584,7 @@ export function usePage(pageKey = 'base') {
|
||||
|
||||
const saveConfig = async ({ quiet = false } = {}) => {
|
||||
if (!canEdit() || !checkForm()) return false
|
||||
saveError.value = ''
|
||||
working.value = 'save'
|
||||
try {
|
||||
const request = buildRequest()
|
||||
@ -588,7 +602,8 @@ export function usePage(pageKey = 'base') {
|
||||
return true
|
||||
} catch (error) {
|
||||
if (isRecentAuthenticationCancelled(error)) return false
|
||||
ElMessage.error(error.message || '配置保存失败')
|
||||
saveError.value = error.message || '配置保存失败'
|
||||
ElMessage.error(saveError.value)
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
@ -597,6 +612,7 @@ export function usePage(pageKey = 'base') {
|
||||
|
||||
const applyConfig = async ({ successMessage = '配置已保存' } = {}) => {
|
||||
if (!canOperate() || !savedGeneration.value) return false
|
||||
saveError.value = ''
|
||||
if (!savedInput || savedInput.text !== editorText.value || savedInput.secret !== secretValue.value) {
|
||||
invalidateCandidate()
|
||||
ElMessage.warning('填写内容已变更,请重新保存配置')
|
||||
@ -638,7 +654,8 @@ export function usePage(pageKey = 'base') {
|
||||
ElMessage.warning(sectionError.value)
|
||||
return false
|
||||
}
|
||||
ElMessage.error(error.message || '配置保存失败')
|
||||
saveError.value = error.message || '配置保存失败'
|
||||
ElMessage.error(saveError.value)
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
@ -646,21 +663,23 @@ export function usePage(pageKey = 'base') {
|
||||
}
|
||||
|
||||
const saveAndApply = async () => {
|
||||
if (pendingCandidate.value) {
|
||||
await continuePending()
|
||||
return
|
||||
if (saveFlow.value || !canOperate()) return false
|
||||
saveFlow.value = pendingCandidate.value ? 'resume' : 'save'
|
||||
try {
|
||||
if (pendingCandidate.value) {
|
||||
if (!pendingCandidate.value.sameSession && !await managePending('resume')) return false
|
||||
return await applyConfig()
|
||||
}
|
||||
const saved = await saveConfig({ quiet: true })
|
||||
return saved ? await applyConfig({ successMessage: '配置已保存' }) : false
|
||||
} finally {
|
||||
saveFlow.value = ''
|
||||
}
|
||||
const saved = await saveConfig({ quiet: true })
|
||||
if (saved) await applyConfig({ successMessage: '配置已保存' })
|
||||
}
|
||||
|
||||
const continuePending = async () => {
|
||||
if (!pendingCandidate.value) return
|
||||
if (!pendingCandidate.value.sameSession) {
|
||||
const resumed = await managePending('resume')
|
||||
if (!resumed) return
|
||||
}
|
||||
await applyConfig()
|
||||
if (!pendingCandidate.value) return false
|
||||
return saveAndApply()
|
||||
}
|
||||
|
||||
const previewRollback = async () => {
|
||||
@ -735,7 +754,7 @@ export function usePage(pageKey = 'base') {
|
||||
}
|
||||
|
||||
const beforeLeave = async () => {
|
||||
if (working.value) {
|
||||
if (busy.value) {
|
||||
ElMessage.warning('当前操作尚未完成,请稍后再切换页面')
|
||||
return false
|
||||
}
|
||||
@ -746,7 +765,7 @@ export function usePage(pageKey = 'base') {
|
||||
'确认离开当前页',
|
||||
{ type: 'warning', confirmButtonText: '离开页面', cancelButtonText: '继续编辑' }
|
||||
)
|
||||
return !working.value
|
||||
return !busy.value
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@ -770,9 +789,10 @@ export function usePage(pageKey = 'base') {
|
||||
|
||||
return {
|
||||
pendingScopeHint,
|
||||
saveFlow, busy, showPendingRecovery, continuingSave,
|
||||
loading, availabilityError, working, status, stateLabel, stateTagType, sectionOptions: sections,
|
||||
selectedSection, currentSection, sectionLoading, editorText, formConfig, formErrors, updateField, updateSecret,
|
||||
sectionError, sensitiveHint, secretField, secretValue,
|
||||
sectionError, saveError, sensitiveHint, secretField, secretValue,
|
||||
confirmed, savedGeneration, pendingCandidate, managePending, validationResult, canWrite, canRollback, canAudit, canExportAudit,
|
||||
backupsLoading, backupsError, backups, rollbackGeneration, rollbackConfirmed, rollbackPreview,
|
||||
auditLoading, auditError, auditItems, loadAll, loadSection, validateConfig, saveConfig, applyConfig, saveAndApply, continuePending,
|
||||
|
||||
@ -444,7 +444,10 @@ test('配置编辑页将验证、候选保存和应用精简为单一保存操
|
||||
assert.doesNotMatch(sharedCss + editorCss, /change-workflow|step-number/)
|
||||
assert.match(template, /@click="saveAndApply"/)
|
||||
assert.match(template, /保存配置/)
|
||||
assert.match(template, /@click="continuePending"/)
|
||||
assert.match(template, /continuingSave \? '继续保存' : '保存配置'/)
|
||||
assert.match(template, /v-if="showPendingRecovery"/)
|
||||
assert.match(template, /v-if="showPendingRecovery && pendingScopeHint"/)
|
||||
assert.match(template, /v-if="saveError"[\s\S]*?:title="saveError"[\s\S]*?type="error"/)
|
||||
assert.match(template, /继续保存/)
|
||||
assert.match(template, /v-if="canWrite" class="editor-footer"/)
|
||||
assert.match(template, /@click="managePending\('cancel'\)"/)
|
||||
@ -498,6 +501,181 @@ test('保存并应用一次点击按候选协议连续完成保存和应用', as
|
||||
assert.equal(page.formConfig.value.listener.port, 2525)
|
||||
})
|
||||
|
||||
test('一键保存的所有异步阶段保持忙碌,不闪现恢复提示或允许重复操作', { timeout: 5000 }, async t => {
|
||||
const stages = Array.from({ length: 7 }, () => ({ entered: deferred(), release: deferred() }))
|
||||
t.after(() => stages.forEach(stage => stage.release.resolve()))
|
||||
let requestIndex = 0
|
||||
let testing = false
|
||||
let pending = null
|
||||
let active = {
|
||||
meta: { generation: 1 },
|
||||
tls: { management: 'custom', mode: 'managed_upload', defaultDirectory: 'config/ssl', customDirectory: 'config/ssl', certificateRef: 'config/ssl/public.crt', privateKeyRef: 'config/ssl/private.key' }
|
||||
}
|
||||
const pause = async () => {
|
||||
if (!testing) return
|
||||
const stage = stages[requestIndex++]
|
||||
assert.ok(stage, '不应产生重复请求')
|
||||
stage.entered.resolve()
|
||||
await stage.release.promise
|
||||
}
|
||||
const { page, successes } = fixture(() => {}, 'ssl', { http: {
|
||||
get: async path => {
|
||||
await pause()
|
||||
return { errorNo: 0, data: path.endsWith('/pending') ? pending : path.endsWith('/status') ? { activeGeneration: active.meta.generation } : active }
|
||||
},
|
||||
put: async (path, body) => {
|
||||
await pause()
|
||||
pending = { generation: 2, candidateId: 'continuous-save', section: 'ssl', sameSession: true, payload: body.payload }
|
||||
return { errorNo: 0, data: pending }
|
||||
},
|
||||
post: async () => {
|
||||
await pause()
|
||||
active = { ...pending.payload, meta: { generation: 2 } }
|
||||
pending = null
|
||||
return { errorNo: 0, data: { generation: 2 } }
|
||||
}
|
||||
} })
|
||||
await page.loadAll()
|
||||
page.updateField(['tls', 'certificateRef'], 'D:/certs/public.crt')
|
||||
const require = createRequire(new URL('../../frontend/package.json', import.meta.url))
|
||||
const { watch } = require('vue')
|
||||
const transitions = []
|
||||
const stop = watch(() => [page.working.value, page.saveFlow.value, page.pendingCandidate.value], () => {
|
||||
transitions.push({ working: page.working.value, flow: page.saveFlow.value, pending: !!page.pendingCandidate.value, busy: page.busy.value, recovery: page.showPendingRecovery.value })
|
||||
}, { flush: 'sync' })
|
||||
t.after(stop)
|
||||
testing = true
|
||||
const saving = page.saveAndApply()
|
||||
for (const stage of stages) {
|
||||
await Promise.race([stage.entered.promise, saving.then(() => { throw new Error('保存流程提前结束') })])
|
||||
assert.equal(page.busy.value, true)
|
||||
assert.equal(page.saveFlow.value, 'save')
|
||||
assert.equal(page.showPendingRecovery.value, false)
|
||||
assert.equal(page.continuingSave.value, false)
|
||||
const input = page.editorText.value
|
||||
page.updateField(['tls', 'certificateRef'], 'D:/blocked.crt')
|
||||
assert.equal(page.editorText.value, input)
|
||||
assert.equal(await page.saveAndApply(), false)
|
||||
assert.equal(await page.continuePending(), false)
|
||||
assert.equal(await page.beforeLeave(), false)
|
||||
stage.release.resolve()
|
||||
}
|
||||
assert.equal(await saving, true)
|
||||
assert.equal(requestIndex, 7)
|
||||
assert.equal(page.busy.value, false)
|
||||
assert.equal(page.saveFlow.value, '')
|
||||
assert.equal(page.showPendingRecovery.value, false)
|
||||
assert.deepEqual(successes, ['配置已保存'])
|
||||
const gaps = transitions.filter(state => state.pending && !state.working && state.flow)
|
||||
assert.ok(gaps.length > 0, '必须检查暂存与应用之间的同步状态切换')
|
||||
assert.ok(gaps.every(state => state.busy && !state.recovery))
|
||||
})
|
||||
|
||||
for (const failure of ['save', 'apply', 'apply-auth-cancel', 'readback']) {
|
||||
test(`一键保存 ${failure} 后解除忙碌,真实失败仍提示且未应用的候选可以恢复`, async () => {
|
||||
let fail = true
|
||||
let applied = false
|
||||
let pending = null
|
||||
let writes = 0
|
||||
const config = {
|
||||
meta: { generation: 1 },
|
||||
tls: { management: 'custom', mode: 'managed_upload', defaultDirectory: 'config/ssl', customDirectory: 'config/ssl', certificateRef: 'config/ssl/public.crt', privateKeyRef: 'config/ssl/private.key' }
|
||||
}
|
||||
const { page, messages, successes } = fixture(() => {}, 'ssl', { http: {
|
||||
get: async path => {
|
||||
if (fail && applied && failure === 'readback') throw new Error('模拟回读失败')
|
||||
return { errorNo: 0, data: path.endsWith('/pending') ? pending : path.endsWith('/status') ? { activeGeneration: applied ? 2 : 1 } : { ...config, meta: { generation: applied ? 2 : 1 } } }
|
||||
},
|
||||
put: async (path, body) => {
|
||||
writes++
|
||||
if (fail && failure === 'save') throw new Error('模拟保存失败')
|
||||
pending = { generation: 2, candidateId: 'failed-save', section: 'ssl', sameSession: true, payload: body.payload }
|
||||
return { errorNo: 0, data: pending }
|
||||
},
|
||||
post: async () => {
|
||||
writes++
|
||||
if (fail && failure === 'apply') throw new Error('模拟应用失败')
|
||||
if (fail && failure === 'apply-auth-cancel') throw Object.assign(new Error('需要最近认证'), { apiErrorCode: 'RECENT_AUTH_REQUIRED' })
|
||||
applied = true
|
||||
pending = null
|
||||
return { errorNo: 0, data: { generation: 2 } }
|
||||
}
|
||||
} })
|
||||
await page.loadAll()
|
||||
page.updateField(['tls', 'certificateRef'], 'D:/certs/public.crt')
|
||||
assert.equal(await page.saveAndApply(), false)
|
||||
assert.equal(page.busy.value, false)
|
||||
assert.equal(page.saveFlow.value, '')
|
||||
assert.deepEqual(successes, [])
|
||||
const recoverable = ['apply', 'apply-auth-cancel'].includes(failure)
|
||||
assert.equal(page.showPendingRecovery.value, recoverable)
|
||||
assert.equal(page.continuingSave.value, recoverable)
|
||||
if (failure === 'apply-auth-cancel') {
|
||||
assert.deepEqual(messages, [])
|
||||
assert.equal(page.saveError.value, '')
|
||||
} else {
|
||||
assert.ok(messages.length > 0, '真实失败不能被隐藏')
|
||||
assert.equal(page.saveError.value || page.sectionError.value, messages.at(-1), '错误必须保留在页面中')
|
||||
}
|
||||
if (failure === 'save') {
|
||||
page.updateField(['tls', 'certificateRef'], 'D:/certs/fixed/public.crt')
|
||||
assert.equal(page.saveError.value, '')
|
||||
}
|
||||
if (recoverable) {
|
||||
fail = false
|
||||
const writesBefore = writes
|
||||
const continuing = page.continuePending()
|
||||
assert.equal(page.saveFlow.value, 'resume')
|
||||
assert.equal(page.continuingSave.value, true)
|
||||
assert.equal(page.showPendingRecovery.value, false)
|
||||
assert.equal(page.saveError.value, '')
|
||||
assert.equal(await continuing, true)
|
||||
assert.equal(writes, writesBefore + 1, '只应用已有候选,不重复保存')
|
||||
assert.equal(page.showPendingRecovery.value, false)
|
||||
assert.deepEqual(successes, ['配置已保存'])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const rejected of [false, true]) {
|
||||
test(`恢复其他会话候选期间保持按钮稳定,恢复失败保留入口:失败=${rejected}`, async () => {
|
||||
let active = {
|
||||
meta: { generation: 1 },
|
||||
tls: { management: 'custom', mode: 'managed_upload', defaultDirectory: 'config/ssl', customDirectory: 'config/ssl', certificateRef: 'config/ssl/public.crt', privateKeyRef: 'config/ssl/private.key' }
|
||||
}
|
||||
let pending = { generation: 2, section: 'ssl', sameSession: false, candidateId: 'another-session', payload: active }
|
||||
const requests = []
|
||||
const { page, messages } = fixture(() => {}, 'ssl', { http: {
|
||||
get: async path => ({ errorNo: 0, data: path.endsWith('/pending') ? pending : path.endsWith('/status') ? { activeGeneration: active.meta.generation } : active }),
|
||||
post: async (path, body) => {
|
||||
assert.equal(page.saveFlow.value, 'resume')
|
||||
assert.equal(page.busy.value, true)
|
||||
assert.equal(page.continuingSave.value, true)
|
||||
assert.equal(page.showPendingRecovery.value, false)
|
||||
requests.push(path)
|
||||
if (path.endsWith('/pending')) {
|
||||
assert.equal(body.action, 'resume')
|
||||
if (rejected) throw new Error('模拟候选恢复失败')
|
||||
pending = { ...pending, sameSession: true }
|
||||
} else {
|
||||
assert.equal(body.candidateId, 'another-session')
|
||||
active = { ...active, meta: { generation: 2 } }
|
||||
pending = null
|
||||
}
|
||||
return { errorNo: 0, data: { generation: 2 } }
|
||||
}
|
||||
} })
|
||||
await page.loadAll()
|
||||
assert.equal(page.showPendingRecovery.value, true)
|
||||
assert.equal(await page.continuePending(), !rejected)
|
||||
assert.equal(page.busy.value, false)
|
||||
assert.equal(page.showPendingRecovery.value, rejected)
|
||||
assert.equal(page.saveError.value, rejected ? '模拟候选恢复失败' : '')
|
||||
assert.deepEqual(requests, rejected ? ['/api/v1/config/pending'] : ['/api/v1/config/pending', '/api/v1/config/ssl/apply'])
|
||||
assert.deepEqual(messages, rejected ? ['模拟候选恢复失败'] : [])
|
||||
})
|
||||
}
|
||||
|
||||
test('最近认证过期时在当前页确认密码并自动重试保存和应用', async () => {
|
||||
const requests = []
|
||||
let pending = null
|
||||
@ -631,7 +809,7 @@ test('标题合并配置状态,保留刷新保护和降级提示,不再渲
|
||||
assert.doesNotMatch(header, /stateLabel|运行状态|state-tag/)
|
||||
assert.match(header, /v-for="issue in status.degradedReasons \|\| \[\]"/)
|
||||
assert.match(header, /v-if="!loading && !availabilityError"/)
|
||||
assert.match(header, /:disabled="!!working \|\| sectionLoading \|\| loading"/)
|
||||
assert.match(header, /:disabled="busy \|\| sectionLoading \|\| loading"/)
|
||||
assert.match(header, /@click="refreshPage"/)
|
||||
assert.doesNotMatch(header, /<dl class="overview-meta"/)
|
||||
assert.doesNotMatch(header, /access-tag/)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user