test: 同步报销审批流与预算分析测试
- 新增预算审批合并、风险标记去重与占位条目校验用例 - 补充预算分析对当前审核人与财务的可见性断言 - 调整单据删除权限测试以匹配 admin 限制
This commit is contained in:
@@ -50,7 +50,7 @@ test('direct approvers can return claims without receiving delete permissions',
|
||||
assert.equal(canManageExpenseClaims(approverUser), false)
|
||||
})
|
||||
|
||||
test('finance can return and final approve, but only executives can manage delete permissions', () => {
|
||||
test('finance can return and final approve, executives can manage claim visibility only', () => {
|
||||
assert.equal(canReturnExpenseClaims({ roleCodes: ['finance'] }), true)
|
||||
assert.equal(canApproveLeaderExpenseClaims({ roleCodes: ['finance'] }), false)
|
||||
assert.equal(canManageExpenseClaims({ roleCodes: ['finance'] }), false)
|
||||
|
||||
@@ -68,6 +68,18 @@ test('budget ontology context maps dialog fields to ontology payload', () => {
|
||||
assert.equal(context.budget_details[0].warning_threshold, '80%')
|
||||
})
|
||||
|
||||
test('budget ontology includes approval review execution metrics', () => {
|
||||
const fieldKeys = BUDGET_ONTOLOGY_FIELDS.map((field) => field.key)
|
||||
|
||||
assert.ok(fieldKeys.includes('claim_amount'))
|
||||
assert.ok(fieldKeys.includes('claim_amount_ratio'))
|
||||
assert.ok(fieldKeys.includes('usage_rate'))
|
||||
assert.ok(fieldKeys.includes('after_usage_rate'))
|
||||
assert.ok(fieldKeys.includes('remaining_budget_ratio'))
|
||||
assert.ok(fieldKeys.includes('available_before_amount'))
|
||||
assert.ok(fieldKeys.includes('over_budget_amount'))
|
||||
})
|
||||
|
||||
test('budget expense type options expose real expense type codes', () => {
|
||||
const optionCodes = BUDGET_EXPENSE_TYPE_OPTIONS.map((item) => item.value)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const requestsComposable = readFileSync(
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('documents center keeps only the top scope tabs and renders status as a dropdown filter', () => {
|
||||
test('documents center keeps only the top scope tabs and renders risk level as a dropdown filter', () => {
|
||||
assert.match(documentsCenterView, /<nav class="status-tabs document-scope-tabs"/)
|
||||
assert.doesNotMatch(documentsCenterView, /<nav class="status-tabs document-state-tabs"/)
|
||||
assert.match(documentsCenterView, /class="document-status-filter"[\s\S]*class="document-filter status-dropdown-filter"/)
|
||||
@@ -35,6 +35,7 @@ test('documents center keeps only the top scope tabs and renders status as a dro
|
||||
)
|
||||
assert.match(documentsCenterView, /v-for="option in statusFilterOptions"/)
|
||||
assert.match(documentsCenterView, /@click="selectStatusTab\(option\.value\)"/)
|
||||
assert.match(documentsCenterView, /aria-label="风险等级"/)
|
||||
})
|
||||
|
||||
test('documents center top tabs start from all and show document category labels', () => {
|
||||
@@ -104,7 +105,7 @@ test('documents center category tabs map to the intended row sources', () => {
|
||||
test('documents center sorts every filtered scope by latest document time before pagination', () => {
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/return sortDocumentRowsByLatestTime\(activeScopeRows\.value\.filter\(\(row\) => \{[\s\S]*matchesKeyword && matchesDocumentType && matchesScene && matchesStatus && matchesDateRange[\s\S]*\}\)\)/
|
||||
/return sortDocumentRowsByLatestTime\(activeScopeRows\.value\.filter\(\(row\) => \{[\s\S]*matchesKeyword && matchesDocumentType && matchesScene && matchesRiskLevel && matchesDateRange[\s\S]*\}\)\)/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
@@ -296,23 +297,23 @@ test('documents center switches filter conditions by category tab', () => {
|
||||
assert.match(documentsCenterView, /const FILTER_CONFIG_BY_SCOPE = \{/)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/\[DOCUMENT_SCOPE_ALL\]: \{[\s\S]*sceneFallbackLabel: '单据场景'[\s\S]*statusTitle: '单据状态'[\s\S]*showDocumentType: true/
|
||||
/\[DOCUMENT_SCOPE_ALL\]: \{[\s\S]*sceneFallbackLabel: '单据场景'[\s\S]*statusTitle: '风险等级'[\s\S]*statusTabs: riskLevelTabs[\s\S]*showDocumentType: true/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/\[DOCUMENT_SCOPE_APPLICATION\]: \{[\s\S]*sceneFallbackLabel: '申请场景'[\s\S]*statusTitle: '申请状态'[\s\S]*showDocumentType: false/
|
||||
/\[DOCUMENT_SCOPE_APPLICATION\]: \{[\s\S]*sceneFallbackLabel: '申请场景'[\s\S]*statusTitle: '风险等级'[\s\S]*statusTabs: riskLevelTabs[\s\S]*showDocumentType: false/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/\[DOCUMENT_SCOPE_REIMBURSEMENT\]: \{[\s\S]*statusTitle: '报销状态'[\s\S]*showDocumentType: false/
|
||||
/\[DOCUMENT_SCOPE_REIMBURSEMENT\]: \{[\s\S]*statusTitle: '风险等级'[\s\S]*statusTabs: riskLevelTabs[\s\S]*showDocumentType: false/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/\[DOCUMENT_SCOPE_REVIEW\]: \{[\s\S]*sceneFallbackLabel: '审核场景'[\s\S]*statusTitle: '审核状态'[\s\S]*statusTabs: \['全部', '审批中', '待补充', '已完成'\]/
|
||||
/\[DOCUMENT_SCOPE_REVIEW\]: \{[\s\S]*sceneFallbackLabel: '审核场景'[\s\S]*statusTitle: '风险等级'[\s\S]*statusTabs: riskLevelTabs/
|
||||
)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
/\[DOCUMENT_SCOPE_ARCHIVE\]: \{[\s\S]*dateLabel: '归档时间'[\s\S]*statusTitle: '归档状态'[\s\S]*statusTabs: \['全部', '已付款', '已完成'\]/
|
||||
/\[DOCUMENT_SCOPE_ARCHIVE\]: \{[\s\S]*dateLabel: '归档时间'[\s\S]*statusTitle: '风险等级'[\s\S]*statusTabs: riskLevelTabs/
|
||||
)
|
||||
assert.match(documentsCenterView, /v-if="showDocumentTypeFilter" class="document-filter"/)
|
||||
assert.match(documentsCenterView, /:placeholder="activeFilterConfig\.searchPlaceholder"/)
|
||||
@@ -326,10 +327,11 @@ test('documents center switches filter conditions by category tab', () => {
|
||||
assert.doesNotMatch(documentsCenterView, /pageSizeOpen/)
|
||||
})
|
||||
|
||||
test('documents center status dropdown derives labels and closes after selection', () => {
|
||||
test('documents center risk dropdown derives labels and closes after selection', () => {
|
||||
assert.match(documentsCenterView, /const riskLevelTabs = \['全部', '高风险', '中风险', '低风险', '无风险'\]/)
|
||||
assert.match(documentsCenterView, /const statusFilterOptions = computed\(\(\) =>/)
|
||||
assert.match(documentsCenterView, /activeFilterConfig\.value\.statusTabs\.map/)
|
||||
assert.match(documentsCenterView, /label: tab === '全部' \? '全部状态' : tab/)
|
||||
assert.match(documentsCenterView, /label: tab === '全部' \? '全部风险' : tab/)
|
||||
assert.match(documentsCenterView, /const statusFilterLabel = computed\(\(\) =>/)
|
||||
assert.match(
|
||||
documentsCenterView,
|
||||
@@ -337,6 +339,18 @@ test('documents center status dropdown derives labels and closes after selection
|
||||
)
|
||||
})
|
||||
|
||||
test('documents center list renders risk level tags instead of status tags', () => {
|
||||
assert.match(documentsCenterView, /<th>风险等级<\/th>/)
|
||||
assert.match(documentsCenterView, /<td data-label="风险等级">[\s\S]*class="risk-level-tags"[\s\S]*v-for="tag in row\.riskTags"/)
|
||||
assert.match(documentsCenterView, /import \{ countClaimRisks, resolveArchiveRiskTone \} from '..\/utils\/archiveCenterListFilters\.js'/)
|
||||
assert.match(documentsCenterView, /function buildDocumentRiskMeta\(row\) \{[\s\S]*countClaimRisks\(riskFlags, riskSummary\)/)
|
||||
assert.match(documentsCenterView, /riskTone: riskMeta\.tone,[\s\S]*riskLabel: riskMeta\.label,[\s\S]*riskCount: riskMeta\.count,[\s\S]*riskTags: riskMeta\.tags/)
|
||||
assert.match(documentsCenterView, /function matchesRiskLevelTab\(row, tab\) \{[\s\S]*tab === '高风险'[\s\S]*row\.riskTone === 'high'/)
|
||||
assert.match(documentListSharedStyles, /\.risk-level-tags\s*\{[\s\S]*display:\s*inline-flex;/)
|
||||
assert.match(documentListSharedStyles, /\.risk-level-tag\.high\s*\{[\s\S]*background:\s*#fef2f2;/)
|
||||
assert.doesNotMatch(documentsCenterView, /<td data-label="状态"><span class="status-tag"/)
|
||||
})
|
||||
|
||||
test('documents center status dropdown uses compact filter styling', () => {
|
||||
assert.match(documentsCenterStyles, /\.documents-list\s*\{[\s\S]*grid-template-rows:\s*auto auto minmax\(0,\s*1fr\) auto;/)
|
||||
assert.match(documentListSharedStyles, /\.status-tabs button\s*\{[\s\S]*display:\s*inline-flex;/)
|
||||
|
||||
@@ -19,6 +19,10 @@ const approvalDialog = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/travel/TravelRequestApprovalDialog.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const confirmDialog = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/shared/ConfirmDialog.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const budgetAnalysisComponent = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/travel/TravelRequestBudgetAnalysis.vue', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -27,6 +31,10 @@ const reimbursementService = readFileSync(
|
||||
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const appShellScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/useAppShell.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
function extractFunction(source, name) {
|
||||
const signatureIndex = source.indexOf(`function ${name}(`)
|
||||
@@ -55,6 +63,9 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
assert.match(detailScript, /approvalMode:/)
|
||||
assert.match(detailScript, /const leaderOpinion = ref\(''\)/)
|
||||
assert.match(detailScript, /const approveConfirmDialogOpen = ref\(false\)/)
|
||||
assert.match(detailScript, /const approvalRiskConfirmed = ref\(false\)/)
|
||||
assert.match(detailScript, /const approvalRiskConfirmItems = computed/)
|
||||
assert.match(detailScript, /const approvalRiskConfirmRequired = computed/)
|
||||
assert.match(detailScript, /const canApproveRequest = computed/)
|
||||
assert.match(detailScript, /canApproveLeaderExpenseClaims/)
|
||||
assert.match(detailScript, /canApproveBudgetExpenseApplications/)
|
||||
@@ -73,8 +84,10 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
assert.doesNotMatch(detailScript, /approvalNextStage/)
|
||||
assert.doesNotMatch(detailScript, /showApplicationLeaderOpinionInput/)
|
||||
assert.doesNotMatch(detailScript, /showLeaderApprovalPanel/)
|
||||
assert.match(detailScript, /const requiresApprovalOpinion = computed\(\(\) => false\)/)
|
||||
assert.match(detailScript, /approvalOpinionTitle = computed\(\(\) => \(isFinanceApprovalStage\.value \? '财务意见' : '附加意见'\)\)/)
|
||||
assert.match(detailScript, /const budgetApprovalOpinionRequired = computed/)
|
||||
assert.match(detailScript, /const requiresApprovalOpinion = computed\(\(\) => budgetApprovalOpinionRequired\.value\)/)
|
||||
assert.match(detailScript, /hasBudgetApprovalWarning\(request\.value\)/)
|
||||
assert.match(detailScript, /return '预算审批意见'/)
|
||||
assert.match(detailScript, /buildLeaderApprovalEvents/)
|
||||
assert.match(detailScript, /buildLeaderApprovalInfo/)
|
||||
assert.match(detailScript, /const leaderApprovalEvents = computed/)
|
||||
@@ -97,6 +110,7 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
assert.match(detailScript, /resolveGeneratedDraftClaimNo/)
|
||||
assert.match(detailScript, /resolveApproveErrorMessage/)
|
||||
assert.match(detailScript, /当前部门未配置 P8 预算审批人,请联系管理员配置后再审批。/)
|
||||
assert.match(detailScript, /预算已超过警戒值,请填写预算审批意见后再通过。/)
|
||||
assert.match(detailScript, /approveActionLabel/)
|
||||
assert.match(detailScript, /approveExpenseClaim\(request\.value\.claimId, \{[\s\S]*opinion: leaderOpinion\.value\.trim\(\) \|\| '同意'/)
|
||||
assert.match(detailScript, /报销草稿 \$\{generatedDraftClaimNo\} 已生成/)
|
||||
@@ -128,6 +142,9 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
assert.match(detailTemplate, /:description="approvalConfirmDescription"/)
|
||||
assert.match(detailTemplate, /:confirm-text="approveConfirmText"/)
|
||||
assert.match(detailTemplate, /:busy-text="approveBusyText"/)
|
||||
assert.match(detailTemplate, /:risk-confirm-required="approvalRiskConfirmRequired"/)
|
||||
assert.match(detailTemplate, /v-model:risk-confirmed="approvalRiskConfirmed"/)
|
||||
assert.match(detailTemplate, /:risk-confirm-items="approvalRiskConfirmItems"/)
|
||||
assert.doesNotMatch(detailTemplate, /:next-stage="approvalNextStage"/)
|
||||
assert.doesNotMatch(approvalDialog, /submit-confirm-summary/)
|
||||
assert.doesNotMatch(approvalDialog, /单据编号/)
|
||||
@@ -144,10 +161,17 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
const confirmApproveRequest = extractFunction(detailScript, 'confirmApproveRequest')
|
||||
assert.doesNotMatch(handleApproveRequest, /approveExpenseClaim/)
|
||||
assert.doesNotMatch(handleApproveRequest, /leaderOpinion\.value\.trim/)
|
||||
assert.match(handleApproveRequest, /approvalRiskConfirmed\.value = !approvalRiskConfirmRequired\.value/)
|
||||
assert.match(confirmApproveRequest, /approveExpenseClaim/)
|
||||
assert.match(confirmApproveRequest, /emit\('request-updated', \{ claimId: request\.value\.claimId \}\)[\s\S]*emit\('backToRequests'\)/)
|
||||
assert.doesNotMatch(confirmApproveRequest, /requiresApprovalOpinion\.value && !leaderOpinion\.value\.trim\(\)/)
|
||||
assert.match(confirmApproveRequest, /approvalRiskConfirmRequired\.value && !approvalRiskConfirmed\.value/)
|
||||
assert.match(confirmApproveRequest, /请先确认已核对风险说明和佐证材料,再继续审批。/)
|
||||
assert.match(confirmApproveRequest, /requiresApprovalOpinion\.value && !leaderOpinion\.value\.trim\(\)/)
|
||||
assert.match(confirmApproveRequest, /预算已超过警戒值,请填写预算审批意见后再通过。/)
|
||||
assert.match(confirmApproveRequest, /emit\('request-updated', \{[\s\S]*claimId: request\.value\.claimId,[\s\S]*claim: responsePayload[\s\S]*\}\)[\s\S]*emit\('backToRequests'\)/)
|
||||
assert.doesNotMatch(confirmApproveRequest, /请先填写领导意见,填写后才能确认审核。/)
|
||||
assert.match(appShellScript, /async function handleRequestUpdated\(payload = \{\}\)/)
|
||||
assert.match(appShellScript, /const mappedRequest = mapExpenseClaimToRequest\(payload\.claim\)/)
|
||||
assert.match(appShellScript, /upsertRequestSnapshot\(mappedRequest\)/)
|
||||
|
||||
assert.match(approvalDialog, /<textarea/)
|
||||
assert.match(approvalDialog, /update:opinion/)
|
||||
@@ -155,6 +179,13 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
assert.match(approvalDialog, /opinionHint/)
|
||||
assert.match(approvalDialog, /opinionRequired/)
|
||||
assert.match(approvalDialog, /\{\{ currentOpinion\.length \}\}\/500/)
|
||||
assert.match(approvalDialog, /风险说明确认/)
|
||||
assert.match(approvalDialog, /riskConfirmRequired/)
|
||||
assert.match(approvalDialog, /update:risk-confirmed/)
|
||||
assert.match(approvalDialog, /:confirm-disabled="confirmDisabled"/)
|
||||
assert.match(approvalDialog, /props\.opinionRequired && !currentOpinion\.value\.trim\(\)/)
|
||||
assert.match(confirmDialog, /confirmDisabled:\s*\{\s*type:\s*Boolean,\s*default:\s*false\s*\}/)
|
||||
assert.match(confirmDialog, /:disabled="busy \|\| confirmDisabled"/)
|
||||
|
||||
assert.match(detailStyles, /\.detail-card-title-with-icon \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/)
|
||||
assert.match(detailStyles, /\.detail-card-title-with-icon i \{[\s\S]*font-size: 18px;[\s\S]*line-height: 1;/)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveRiskTagTone
|
||||
} from '../src/views/scripts/travelRequestDetailInsights.js'
|
||||
import {
|
||||
buildExpenseDraftIssues,
|
||||
buildExpenseItemViewModel,
|
||||
buildDraftBlockingIssues,
|
||||
rebuildExpenseItems,
|
||||
@@ -27,7 +28,8 @@ import {
|
||||
} from '../src/views/scripts/travelRequestDetailAdviceModel.js'
|
||||
import {
|
||||
buildStandardAdjustmentPayload,
|
||||
filterSubmitterResolvedRiskCards
|
||||
filterSubmitterResolvedRiskCards,
|
||||
isRiskCardMissingExpenseNote
|
||||
} from '../src/views/scripts/travelRequestDetailStandardAdjustment.js'
|
||||
|
||||
const detailViewTemplate = readFileSync(
|
||||
@@ -70,6 +72,10 @@ const stageRiskAdviceCard = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/travel/StageRiskAdviceCard.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const stageRiskAdviceStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/components/stage-risk-advice-card.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
const attachmentMeta = {
|
||||
file_name: 'taxi-invoice.pdf',
|
||||
@@ -291,20 +297,56 @@ test('risk cards carry structured business stage for approval advice filtering',
|
||||
assert.deepEqual(filterRiskCardsByBusinessStage(attachmentSummaryCards, 'expense_application'), [])
|
||||
})
|
||||
|
||||
test('stage risk advice card exposes direct reviewer action suggestion', () => {
|
||||
assert.match(stageRiskAdviceCard, /class="employee-risk-action"/)
|
||||
test('stage risk advice card focuses on document risks without profile or budget boards', () => {
|
||||
assert.match(stageRiskAdviceCard, /employee-risk-decision-panel/)
|
||||
assert.match(stageRiskAdviceCard, /综合审核结论/)
|
||||
assert.match(stageRiskAdviceCard, /建议结论/)
|
||||
assert.match(stageRiskAdviceCard, /\{\{ decisionAction \}\}/)
|
||||
assert.match(stageRiskAdviceCard, /compactEvidenceItems/)
|
||||
assert.match(stageRiskAdviceCard, /compactAdviceItems/)
|
||||
assert.ok(
|
||||
stageRiskAdviceCard.indexOf('class="employee-risk-advice-list"')
|
||||
< stageRiskAdviceCard.indexOf('class="employee-risk-action"')
|
||||
)
|
||||
assert.match(stageRiskAdviceCard, /stageBasisTitle/)
|
||||
assert.match(stageRiskAdviceCard, /stageBasisHint/)
|
||||
assert.match(stageRiskAdviceCard, /employee-risk-profile-section/)
|
||||
assert.match(stageRiskAdviceCard, /employee-risk-profile-list/)
|
||||
assert.match(stageRiskAdviceCard, /classifyReimbursementRiskCards/)
|
||||
assert.match(stageRiskAdviceCard, /stripEmbeddedExplanationText/)
|
||||
assert.match(stageRiskAdviceCard, /if \(summary\) \{[\s\S]*return \[`已补充异常说明:\$\{summary\}`\]/)
|
||||
assert.match(stageRiskAdviceCard, /employee-risk-tone-pill/)
|
||||
assert.match(stageRiskAdviceCard, /\.employee-risk-ai-note \{[\s\S]*grid-template-columns: minmax\(0, 1fr\);/)
|
||||
assert.match(stageRiskAdviceCard, /\.employee-risk-action \{[\s\S]*align-items: center;[\s\S]*justify-content: center;[\s\S]*text-align: center;/)
|
||||
assert.match(stageRiskAdviceStyles, /\.employee-risk-decision-panel \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) minmax\(220px, 32%\);/)
|
||||
assert.match(stageRiskAdviceStyles, /\.employee-risk-profile-list \{[\s\S]*grid-template-columns: 1fr;/)
|
||||
assert.match(stageRiskAdviceStyles, /\.employee-risk-evidence-row li \{[\s\S]*white-space: normal;/)
|
||||
assert.doesNotMatch(stageRiskAdviceStyles, /grid-row: span 2/)
|
||||
assert.match(stageRiskAdviceCard, /建议退回补充票据、行程说明或超标原因/)
|
||||
assert.match(stageRiskAdviceCard, /riskExplanationItems/)
|
||||
assert.match(stageRiskAdviceCard, /请核对已补充说明是否覆盖风险点/)
|
||||
assert.match(stageRiskAdviceCard, /已补充异常说明/)
|
||||
assert.match(stageRiskAdviceCard, /可按权限继续审批/)
|
||||
assert.match(stageRiskAdviceCard, /申请单风险依据/)
|
||||
assert.match(stageRiskAdviceCard, /报销单风险依据/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /人员行为画像/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /部门预算执行/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /title: '说明与佐证'/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /fetchExpenseClaimBudgetAnalysis/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /reviewDimensionCards/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /documentRiskMetrics/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /profileAdviceItems/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /profileContextItems/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /画像风险/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /退单\/补正/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /材料质量/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /申请人:/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /部门\/岗位:/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /budgetContextMetrics/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /BUDGET_FIELD_KEYS/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /预算池/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /未匹配预算池/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /科目未管控/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /占用比例/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /剩余比例/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /超预算风险/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /explanationContextMetrics/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /employee-risk-context-grid/)
|
||||
assert.doesNotMatch(stageRiskAdviceCard, /class="employee-risk-action"/)
|
||||
assert.doesNotMatch(stageRiskAdviceStyles, /employee-risk-ai-note/)
|
||||
})
|
||||
|
||||
test('AI advice ignores approval opinions and flow logs as risks', () => {
|
||||
@@ -489,6 +531,42 @@ test('AI advice view model sorts and displays every risk card', () => {
|
||||
assert.deepEqual(riskSection.items.map((item) => item.id), ['risk-2', 'risk-4', 'risk-1', 'risk-3'])
|
||||
})
|
||||
|
||||
test('AI advice hides lower severity duplicate route explanation risks', () => {
|
||||
const advice = buildAiAdviceViewModel({
|
||||
riskCards: [
|
||||
{
|
||||
id: 'route-high',
|
||||
tone: 'high',
|
||||
label: '高风险',
|
||||
title: '多城市行程待说明',
|
||||
risk: '检测到本次差旅涉及 深圳 多个目的地,但当前报销事由未说明中转、多地拜访或改签原因。',
|
||||
itemIds: ['train-transfer', 'train-transfer-return']
|
||||
},
|
||||
{
|
||||
id: 'route-medium',
|
||||
tone: 'medium',
|
||||
label: '中风险',
|
||||
title: '多城市行程缺少说明中风险',
|
||||
risk: '本次报销识别到多城市行程(上海、武汉、深圳),但事由中未说明中转、多地拜访或改签原因。',
|
||||
itemIds: ['train-transfer', 'train-transfer-return']
|
||||
},
|
||||
{
|
||||
id: 'hotel-high',
|
||||
tone: 'high',
|
||||
label: '高风险',
|
||||
title: '住宿金额超出报销标准',
|
||||
risk: '住宿金额超出当前职级报销标准。',
|
||||
itemIds: ['hotel-item']
|
||||
}
|
||||
]
|
||||
})
|
||||
const riskSection = advice.sections.find((section) => section.kind === 'risk')
|
||||
|
||||
assert.deepEqual(advice.riskCards.map((item) => item.id), ['route-high', 'hotel-high'])
|
||||
assert.deepEqual(riskSection.items.map((item) => item.id), ['route-high', 'hotel-high'])
|
||||
assert.equal(riskSection.totalCount, 2)
|
||||
})
|
||||
|
||||
test('AI advice view model omits empty sections', () => {
|
||||
const readyAdvice = buildAiAdviceViewModel({
|
||||
completionItems: [],
|
||||
@@ -640,6 +718,84 @@ test('route-level risk cards keep related item ids for every affected expense ro
|
||||
assert.match(detailViewScript, /cardItemIds\.includes\(itemId\)/)
|
||||
})
|
||||
|
||||
test('claim risk cards expose related expense explanations to reviewers', () => {
|
||||
const riskCards = buildAttachmentRiskCards({
|
||||
expenseItems: [
|
||||
{
|
||||
id: 'hotel-row',
|
||||
name: '住宿票',
|
||||
desc: '上海喜来登酒店',
|
||||
itemNote: '时间紧,没有合适的酒店,只能住宿超过金额的酒店。'
|
||||
},
|
||||
{
|
||||
id: 'route-extra-out',
|
||||
name: '火车票',
|
||||
desc: '上海-深圳',
|
||||
itemNote: '中间去深圳,公司要求。'
|
||||
},
|
||||
{
|
||||
id: 'route-extra-back',
|
||||
name: '火车票',
|
||||
desc: '深圳-上海',
|
||||
itemNote: '中间去深圳,公司要求。'
|
||||
}
|
||||
],
|
||||
claimRiskFlags: [
|
||||
{
|
||||
source: 'submission_review',
|
||||
severity: 'high',
|
||||
label: '多城市行程待说明',
|
||||
message: '检测到本次差旅涉及 深圳 多个目的地,但当前报销事由未说明中转、多地拜访或改签原因。',
|
||||
item_ids: ['route-extra-out', 'route-extra-back']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.equal(riskCards.length, 1)
|
||||
assert.deepEqual(riskCards[0].itemIds, ['route-extra-out', 'route-extra-back'])
|
||||
assert.match(riskCards[0].risk, /用户已在相关费用明细补充异常说明/)
|
||||
assert.doesNotMatch(riskCards[0].risk, /未说明/)
|
||||
assert.match(riskCards[0].suggestion, /用户已在费用明细补充异常说明/)
|
||||
assert.match(riskCards[0].suggestion, /上海-深圳:中间去深圳,公司要求/)
|
||||
assert.match(riskCards[0].relatedExplanationSummary, /深圳-上海:中间去深圳,公司要求/)
|
||||
assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('用户已补充异常说明')))
|
||||
})
|
||||
|
||||
test('claim risk cards infer hotel explanations when risk flag has no item ids', () => {
|
||||
const riskCards = buildAttachmentRiskCards({
|
||||
expenseItems: [
|
||||
{
|
||||
id: 'hotel-row',
|
||||
name: '住宿票',
|
||||
desc: '上海喜来登酒店',
|
||||
itemType: 'hotel_ticket',
|
||||
itemNote: '时间紧,没有合适的酒店,只能住宿超过金额的酒店。'
|
||||
},
|
||||
{
|
||||
id: 'route-row',
|
||||
name: '火车票',
|
||||
desc: '上海-深圳',
|
||||
itemType: 'train_ticket',
|
||||
itemNote: '中间去深圳,公司要求。'
|
||||
}
|
||||
],
|
||||
claimRiskFlags: [
|
||||
{
|
||||
source: 'submission_review',
|
||||
severity: 'high',
|
||||
label: '住宿金额超出报销标准',
|
||||
message: '住宿标准:P5在上海的住宿标准为 250.00 元/晚,票据识别金额 1086.00 元 / 3 晚,约 362.00 元/晚,超出 112.00 元/晚。'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.equal(riskCards.length, 1)
|
||||
assert.deepEqual(riskCards[0].itemIds, ['hotel-row'])
|
||||
assert.match(riskCards[0].relatedExplanationSummary, /上海喜来登酒店:时间紧,没有合适的酒店/)
|
||||
assert.doesNotMatch(riskCards[0].relatedExplanationSummary, /上海-深圳/)
|
||||
assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('用户已补充异常说明')))
|
||||
})
|
||||
|
||||
test('legacy route-level risk cards infer affected travel rows when backend has no item ids', () => {
|
||||
const riskCards = buildAttachmentRiskCards({
|
||||
expenseItems: [
|
||||
@@ -853,6 +1009,45 @@ test('ticket item types and system allowance row are visible but read only', ()
|
||||
assert.match(detailViewScript, /系统自动计算的补贴行不能删除/)
|
||||
})
|
||||
|
||||
test('expense item rebuild hides empty placeholders but keeps generated allowance row', () => {
|
||||
const items = rebuildExpenseItems(
|
||||
[
|
||||
{
|
||||
id: 'hotel-uploaded',
|
||||
itemType: 'hotel_ticket',
|
||||
itemDate: '2026-02-20',
|
||||
itemReason: '上海喜来登酒店',
|
||||
itemLocation: '上海',
|
||||
itemAmount: 1086,
|
||||
invoiceId: 'claim-1/hotel-uploaded/hotel-invoice.png'
|
||||
},
|
||||
{
|
||||
id: 'empty-travel-placeholder',
|
||||
itemType: 'travel',
|
||||
itemDate: '2026-02-23',
|
||||
itemReason: '',
|
||||
itemLocation: '',
|
||||
itemAmount: 0,
|
||||
invoiceId: ''
|
||||
},
|
||||
{
|
||||
id: 'allowance',
|
||||
itemType: 'travel_allowance',
|
||||
itemDate: '2026-02-23',
|
||||
itemReason: '系统自动计算出差补贴:上海,4天,100.00元/天',
|
||||
itemLocation: '直辖市/特区',
|
||||
itemAmount: 400,
|
||||
invoiceId: ''
|
||||
}
|
||||
],
|
||||
{ typeCode: 'travel', detailVariant: 'travel' }
|
||||
)
|
||||
|
||||
assert.deepEqual(items.map((item) => item.id), ['hotel-uploaded', 'allowance'])
|
||||
assert.equal(items[1].isSystemGenerated, true)
|
||||
assert.equal(items[1].attachmentStatus, '无需附件')
|
||||
})
|
||||
|
||||
test('travel item date caption distinguishes departure return and trip events', () => {
|
||||
assert.match(detailViewTemplate, /<span>\{\{ item\.dayLabel \}\}<\/span>/)
|
||||
assert.match(detailViewScript, /const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set\(\['train_ticket', 'flight_ticket'\]\)/)
|
||||
@@ -1114,6 +1309,26 @@ test('standard adjustment resolves submitter risk prompt only after accepted whi
|
||||
)
|
||||
})
|
||||
|
||||
test('multi item risk is not missing explanation when every related row has note', () => {
|
||||
const card = {
|
||||
id: 'risk-multi-city',
|
||||
itemIds: ['route-extra-out', 'route-extra-back'],
|
||||
tone: 'high',
|
||||
risk: '多城市行程待说明。'
|
||||
}
|
||||
const explainedItems = [
|
||||
{ id: 'route-extra-out', itemNote: '中间去深圳,项目现场要求。' },
|
||||
{ id: 'route-extra-back', itemNote: '从深圳返回上海继续支撑部署。' }
|
||||
]
|
||||
const partlyMissingItems = [
|
||||
{ id: 'route-extra-out', itemNote: '中间去深圳,项目现场要求。' },
|
||||
{ id: 'route-extra-back', itemNote: '' }
|
||||
]
|
||||
|
||||
assert.equal(isRiskCardMissingExpenseNote(card, explainedItems), false)
|
||||
assert.equal(isRiskCardMissingExpenseNote(card, partlyMissingItems), true)
|
||||
})
|
||||
|
||||
test('expense item upload remains limited to one receipt per detail row', () => {
|
||||
assert.match(detailViewTemplate, /ref="expenseUploadInput"[\s\S]*type="file"/)
|
||||
assert.doesNotMatch(
|
||||
@@ -1376,6 +1591,100 @@ test('draft submit validation uses expense detail date and amount when claim sum
|
||||
assert.ok(!issues.some((issue) => issue.includes('报销事由未完善')))
|
||||
})
|
||||
|
||||
test('draft submit validation does not hard block uploaded receipt rows with OCR gaps', () => {
|
||||
const issues = buildDraftBlockingIssues(
|
||||
{
|
||||
profileName: '张三',
|
||||
typeLabel: '住宿费',
|
||||
typeCode: 'hotel',
|
||||
reason: '上海出差住宿',
|
||||
location: '上海',
|
||||
occurredDisplay: '2026-06-01',
|
||||
amountValue: 1086
|
||||
},
|
||||
[
|
||||
buildExpenseItemViewModel(
|
||||
{
|
||||
id: 'hotel-uploaded',
|
||||
itemType: 'hotel_ticket',
|
||||
itemReason: '',
|
||||
itemDate: '',
|
||||
itemAmount: 0,
|
||||
invoiceId: 'claim-1/hotel-uploaded/hotel-invoice.png'
|
||||
},
|
||||
0,
|
||||
{ typeCode: 'hotel', detailVariant: 'travel' }
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert.ok(!issues.some((issue) => issue.includes('缺少日期')))
|
||||
assert.ok(!issues.some((issue) => issue.includes('缺少说明')))
|
||||
assert.ok(!issues.some((issue) => issue.includes('缺少金额')))
|
||||
assert.ok(!issues.some((issue) => issue.includes('缺少票据标识')))
|
||||
})
|
||||
|
||||
test('draft submit validation ignores trailing placeholder detail rows', () => {
|
||||
const issues = buildDraftBlockingIssues(
|
||||
{
|
||||
profileName: '张三',
|
||||
typeLabel: '差旅费',
|
||||
typeCode: 'travel',
|
||||
reason: '上海出差',
|
||||
location: '上海',
|
||||
occurredDisplay: '2026-02-20 至 2026-02-23',
|
||||
amountValue: 1086
|
||||
},
|
||||
[
|
||||
buildExpenseItemViewModel(
|
||||
{
|
||||
id: 'hotel-uploaded',
|
||||
itemType: 'hotel_ticket',
|
||||
itemReason: '上海喜来登酒店',
|
||||
itemLocation: '上海',
|
||||
itemDate: '2026-02-20',
|
||||
itemAmount: 1086,
|
||||
invoiceId: 'claim-1/hotel-uploaded/hotel-invoice.png'
|
||||
},
|
||||
0,
|
||||
{ typeCode: 'travel', detailVariant: 'travel' }
|
||||
),
|
||||
buildExpenseItemViewModel(
|
||||
{
|
||||
id: 'placeholder-6',
|
||||
itemType: 'hotel_ticket',
|
||||
itemDate: '2026-02-23',
|
||||
itemReason: '',
|
||||
itemLocation: '',
|
||||
itemAmount: 0,
|
||||
invoiceId: ''
|
||||
},
|
||||
1,
|
||||
{ typeCode: 'travel', detailVariant: 'travel' }
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert.ok(!issues.some((issue) => issue.includes('费用明细第 2 条缺少说明')))
|
||||
assert.ok(!issues.some((issue) => issue.includes('费用明细第 2 条缺少地点')))
|
||||
assert.ok(!issues.some((issue) => issue.includes('费用明细第 2 条缺少金额')))
|
||||
assert.ok(!issues.some((issue) => issue.includes('费用明细第 2 条缺少票据标识')))
|
||||
})
|
||||
|
||||
test('draft submit validation does not require receipt fields for generated allowance rows', () => {
|
||||
const issues = buildExpenseDraftIssues({
|
||||
id: 'allowance',
|
||||
itemType: 'travel_allowance',
|
||||
itemDate: '2026-02-23',
|
||||
itemReason: '',
|
||||
itemLocation: '',
|
||||
itemAmount: 0,
|
||||
invoiceId: ''
|
||||
})
|
||||
|
||||
assert.deepEqual(issues, [])
|
||||
})
|
||||
|
||||
test('returned application submit validation does not require expense detail rows', () => {
|
||||
const issues = buildDraftBlockingIssues(
|
||||
{
|
||||
|
||||
@@ -48,6 +48,8 @@ function extractFunction(source, name) {
|
||||
|
||||
test('detail submit opens a confirmation dialog before calling submit API', () => {
|
||||
assert.match(detailViewTemplate, /<ConfirmDialog[\s\S]*:open="submitConfirmDialogOpen"[\s\S]*:confirm-text="submitConfirmText"[\s\S]*@close="closeSubmitConfirmDialog"[\s\S]*@confirm="confirmSubmitRequest"/)
|
||||
assert.match(detailViewTemplate, /:secondary-text="submitConfirmSecondaryText"/)
|
||||
assert.match(detailViewTemplate, /@secondary="confirmStandardAdjustment"/)
|
||||
assert.match(detailViewTemplate, /:open="submitConfirmDialogOpen"[\s\S]*size="review"/)
|
||||
assert.match(detailViewTemplate, /cancel-text="返回核对"/)
|
||||
assert.match(detailViewTemplate, /@click="handleSubmit"/)
|
||||
@@ -56,6 +58,7 @@ test('detail submit opens a confirmation dialog before calling submit API', () =
|
||||
assert.match(detailViewTemplate, /v-if="submitBusy" class="expense-recognition-banner submit-progress-banner"/)
|
||||
assert.doesNotMatch(detailViewScript, /preReviewExpenseClaim\(request\.value\.claimId\)/)
|
||||
assert.match(detailViewScript, /const submitActionLabel = computed/)
|
||||
assert.match(detailViewScript, /const submitConfirmSecondaryText = computed/)
|
||||
assert.match(detailViewScript, /submitConfirmDialogOpen\.value = true/)
|
||||
assert.match(detailViewScript, /submitConfirmDialogOpen\.value = false/)
|
||||
assert.match(detailViewScript, /submitConfirmDialogOpen,/)
|
||||
@@ -78,23 +81,38 @@ test('detail submit warns on missing risk explanation and supports standard adju
|
||||
assert.match(detailViewTemplate, /:open="riskOverrideDialogOpen"/)
|
||||
assert.match(detailViewTemplate, /:open="riskOverrideDialogOpen"[\s\S]*size="review"/)
|
||||
assert.match(detailViewTemplate, /异常说明/)
|
||||
assert.match(detailViewTemplate, /按职级标准重算/)
|
||||
assert.match(detailViewTemplate, /:title="riskOverrideDialogTitle"/)
|
||||
assert.match(detailViewTemplate, /:description="riskOverrideDialogDescription"/)
|
||||
assert.match(detailViewTemplate, /:confirm-text="riskOverrideConfirmText"/)
|
||||
assert.match(detailViewTemplate, /@confirm="confirmRiskOverrideDialog"/)
|
||||
assert.match(detailViewTemplate, /class="risk-override-card-shell"/)
|
||||
assert.match(detailViewTemplate, /class="risk-override-side-nav risk-override-side-nav--previous"/)
|
||||
assert.match(detailViewTemplate, /class="risk-override-side-nav risk-override-side-nav--next"/)
|
||||
assert.match(detailViewTemplate, /class="risk-override-guidance"/)
|
||||
assert.match(detailViewTemplate, /goToPreviousSubmitRisk/)
|
||||
assert.match(detailViewTemplate, /goToNextSubmitRisk/)
|
||||
assert.doesNotMatch(detailViewTemplate, /class="risk-override-nav"/)
|
||||
assert.doesNotMatch(detailViewTemplate, /v-model="riskOverrideReasons\[currentSubmitRiskWarning\.id\]"/)
|
||||
assert.doesNotMatch(detailViewTemplate, /risk-override-save-btn/)
|
||||
assert.doesNotMatch(detailViewTemplate, /confirmRiskOverrideReasons/)
|
||||
assert.match(detailViewScript, /const submitRiskWarnings = computed/)
|
||||
assert.match(detailViewScript, /const submitExplainedRiskWarnings = computed/)
|
||||
assert.match(detailViewScript, /const submitRiskReviewWarnings = computed/)
|
||||
assert.match(detailViewScript, /const hasMissingSubmitRiskWarnings = computed/)
|
||||
assert.match(detailViewScript, /const riskOverrideConfirmText = computed\(\(\) =>[\s\S]*确认说明/)
|
||||
const handleSubmit = extractFunction(detailViewScript, 'handleSubmit')
|
||||
const confirmSubmitRequest = extractFunction(detailViewScript, 'confirmSubmitRequest')
|
||||
assert.match(handleSubmit, /submitRiskWarnings\.value\.length[\s\S]*openRiskOverrideDialog\(\)/)
|
||||
assert.match(handleSubmit, /submitRiskReviewWarnings\.value\.length[\s\S]*openRiskOverrideDialog\(\)/)
|
||||
assert.doesNotMatch(confirmSubmitRequest, /openRiskOverrideDialog/)
|
||||
assert.doesNotMatch(detailViewScript, /riskOverrideReasons/)
|
||||
assert.doesNotMatch(detailViewScript, /function confirmRiskOverrideReasons\(\)/)
|
||||
assert.match(detailViewScript, /function confirmRiskOverrideDialog\(\)/)
|
||||
assert.match(detailViewScript, /function confirmRiskExplanation\(\)/)
|
||||
assert.match(detailViewScript, /function confirmStandardAdjustment\(\)/)
|
||||
assert.match(detailViewTemplate, /v-if="standardAdjustmentBusy" class="expense-recognition-banner standard-adjustment-banner"/)
|
||||
assert.match(detailViewScript, /const standardAdjustmentBusy = ref\(false\)/)
|
||||
const confirmRiskExplanation = extractFunction(detailViewScript, 'confirmRiskExplanation')
|
||||
assert.match(confirmRiskExplanation, /riskOverrideDialogOpen\.value = false[\s\S]*submitConfirmDialogOpen\.value = true/)
|
||||
const confirmStandardAdjustment = extractFunction(detailViewScript, 'confirmStandardAdjustment')
|
||||
assert.match(confirmStandardAdjustment, /const claimId = String\(request\.value\?\.claimId/)
|
||||
assert.match(confirmStandardAdjustment, /riskOverrideDialogOpen\.value = false[\s\S]*standardAdjustmentBusy\.value = true[\s\S]*void runStandardAdjustmentRecalculation\(claimId, taskSeq\)/)
|
||||
@@ -103,6 +121,7 @@ test('detail submit warns on missing risk explanation and supports standard adju
|
||||
const runStandardAdjustmentRecalculation = extractFunction(detailViewScript, 'runStandardAdjustmentRecalculation')
|
||||
assert.match(runStandardAdjustmentRecalculation, /acceptExpenseClaimStandardAdjustment\(claimId, payload\)/)
|
||||
assert.doesNotMatch(runStandardAdjustmentRecalculation, /submitConfirmDialogOpen\.value = true/)
|
||||
assert.match(detailViewScript, /buildStandardAdjustmentPayloadModel\(\{[\s\S]*warnings:\s*submitRiskCards\.value/)
|
||||
const actionBusyStart = detailViewScript.indexOf('const actionBusy = computed')
|
||||
const actionBusyEnd = detailViewScript.indexOf('const profile = computed', actionBusyStart)
|
||||
assert.ok(actionBusyStart > -1 && actionBusyEnd > actionBusyStart)
|
||||
@@ -126,20 +145,15 @@ test('detail header and fallback progress use reimbursement wording', () => {
|
||||
assert.doesNotMatch(detailViewScript, /label:\s*'保存草稿'/)
|
||||
})
|
||||
|
||||
test('archived detail delete action is gated by admin-only permission', () => {
|
||||
assert.match(detailViewScript, /canDeleteArchivedExpenseClaims/)
|
||||
assert.match(detailViewScript, /isArchivedRequestView/)
|
||||
assert.match(detailViewScript, /if \(isArchivedRequest\.value\) {\s*return canDeleteArchivedExpenseClaims\(currentUser\.value\)/)
|
||||
test('detail delete action is gated by admin-only permission', () => {
|
||||
assert.match(detailViewScript, /const canDeleteRequest = computed\(\(\) => isPlatformAdminUser\(currentUser\.value\)\)/)
|
||||
assert.match(detailViewTemplate, /v-else-if="canReturnRequest \|\| canApproveRequest \|\| canPayRequest \|\| canDeleteRequest"/)
|
||||
assert.doesNotMatch(detailViewTemplate, /v-if="canManageCurrentClaim"/)
|
||||
})
|
||||
|
||||
test('editable detail delete action is limited to applicant or claim manager', () => {
|
||||
assert.match(detailViewScript, /const isCurrentApplicant = computed/)
|
||||
assert.match(detailViewScript, /isPlatformAdminUser/)
|
||||
assert.match(detailViewScript, /if \(isApplicationDocument\.value\) {\s*return isPlatformAdminUser\(currentUser\.value\) \|\| \(isEditableRequest\.value && isCurrentApplicant\.value\)\s*}/)
|
||||
assert.match(detailViewScript, /if \(canManageCurrentClaim\.value\) {\s*return true\s*}/)
|
||||
assert.match(detailViewScript, /return isEditableRequest\.value && isCurrentApplicant\.value/)
|
||||
test('detail delete action does not allow applicant or claim manager fallback', () => {
|
||||
assert.doesNotMatch(detailViewScript, /const canDeleteRequest = computed\(\(\) => \{[\s\S]*isCurrentApplicant[\s\S]*\}\)/)
|
||||
assert.doesNotMatch(detailViewScript, /const canDeleteRequest = computed\(\(\) => \{[\s\S]*canManageCurrentClaim[\s\S]*\}\)/)
|
||||
assert.match(detailViewScript, /if \(isApplicationDocument\.value\) {\s*return '删除申请'\s*}/)
|
||||
assert.match(detailViewScript, /当前申请单已进入审批流程,只有退回后申请人本人或系统管理员可以删除。/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user