import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { ref } from 'vue' import { buildReviewFormContextFromPayload, buildLocallySyncedReviewPayload, buildReviewNextStepRichCopy, buildReviewPlainFollowupCopy, isTravelReviewPayload, resolveReviewFooterActions } from '../src/views/scripts/travelReimbursementReviewModel.js' import { useTravelReimbursementAttachments } from '../src/views/scripts/useTravelReimbursementAttachments.js' import { renderMarkdown } from '../src/utils/markdown.js' const createViewTemplate = readFileSync( fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)), 'utf8' ) const createViewScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)), 'utf8' ) const createViewScriptSurface = [ '../src/views/scripts/TravelReimbursementCreateView.js', '../src/views/scripts/useTravelReimbursementComposerTools.js', '../src/views/scripts/useTravelReimbursementCreateViewControls.js', '../src/views/scripts/useTravelReimbursementCreateViewDrawerControls.js', '../src/views/scripts/useTravelReimbursementCreateViewLifecycle.js', '../src/views/scripts/useTravelReimbursementCreateViewMessageHandlers.js', '../src/views/scripts/useTravelReimbursementCreateViewState.js', '../src/views/scripts/useTravelReimbursementCreateViewTravelCalculator.js', '../src/views/scripts/useTravelReimbursementCreateViewUi.js', '../src/views/scripts/useTravelReimbursementMessageActions.js', '../src/views/scripts/useTravelReimbursementReviewActions.js' ].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n') const reviewPanelModelScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/travelReimbursementReviewPanelModel.js', import.meta.url)), 'utf8' ) const messageItemTemplate = readFileSync( fileURLToPath(new URL('../src/components/travel/TravelReimbursementMessageItem.vue', import.meta.url)), 'utf8' ) const insightPanelTemplate = readFileSync( fileURLToPath(new URL('../src/components/travel/TravelReimbursementInsightPanel.vue', import.meta.url)), 'utf8' ) const createViewTemplateSurface = [ createViewTemplate, messageItemTemplate, insightPanelTemplate ].join('\n') const reimbursementService = readFileSync( fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)), 'utf8' ) const reimbursementFlowScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)), 'utf8' ) const flowTimingScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/travelReimbursementFlowTiming.js', import.meta.url)), 'utf8' ) const reviewActionsScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementReviewActions.js', import.meta.url)), 'utf8' ) const reviewDrawerScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementReviewDrawer.js', import.meta.url)), 'utf8' ) const submitComposerScript = [ '../src/views/scripts/travelReimbursementSubmitConstants.js', '../src/views/scripts/travelReimbursementSubmitApplicationConflicts.js', '../src/views/scripts/travelReimbursementSubmitApplicationPreview.js', '../src/views/scripts/travelReimbursementSubmitLocalPreviewFlow.js', '../src/views/scripts/travelReimbursementSubmitStewardDelegation.js', '../src/views/scripts/travelReimbursementSubmitAttachmentFlow.js', '../src/views/scripts/travelReimbursementSubmitDraftPreflight.js', '../src/views/scripts/travelReimbursementSubmitRecognitionFlow.js', '../src/views/scripts/travelReimbursementSubmitResponseModel.js', '../src/views/scripts/useTravelReimbursementSubmitComposer.js' ].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n') const attachmentsScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementAttachments.js', import.meta.url)), 'utf8' ) const attachmentSyncScript = readFileSync( fileURLToPath(new URL('../src/utils/expenseClaimAttachmentSync.js', import.meta.url)), 'utf8' ) const sessionStateScript = readFileSync( fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSessionState.js', import.meta.url)), 'utf8' ) const createViewBaseStyles = readFileSync( fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view.css', import.meta.url)), 'utf8' ) const createViewPart2Styles = readFileSync( fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part2.css', import.meta.url)), 'utf8' ) const createViewPart3Styles = readFileSync( fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part3.css', import.meta.url)), 'utf8' ) const createViewPart4Styles = readFileSync( fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part4.css', import.meta.url)), 'utf8' ) const insightPanelStyles = readFileSync( fileURLToPath(new URL('../src/assets/styles/components/travel-reimbursement-insight-panel.css', import.meta.url)), 'utf8' ) test('review drawer tools expose the default review tab before conditional document and risk tabs', () => { assert.match(createViewTemplateSurface, /v-if="ui\.activeReviewPayload && ui\.reviewOverviewDrawerAvailable"[\s\S]*title="报销识别核对"[\s\S]*@click="ui\.switchToReviewOverviewDrawer"/) assert.match(createViewTemplateSurface, /v-if="ui\.activeReviewPayload && ui\.reviewDocumentDrawerAvailable"[\s\S]*title="单据识别"/) assert.match(createViewTemplateSurface, /v-if="ui\.activeReviewPayload && ui\.reviewRiskDrawerAvailable"[\s\S]*title="显示风险"/) assert.match(createViewTemplateSurface, /title="调用流程"/) assert.ok( createViewTemplateSurface.indexOf('title="报销识别核对"') < createViewTemplateSurface.indexOf('title="单据识别"'), 'default review button should be placed before the document recognition button' ) }) test('review drawer tool buttons switch modes instead of toggling the active mode closed', () => { assert.match(createViewScriptSurface, /const isReviewOverviewDrawer = computed\(\(\) => reviewDrawerMode\.value === REVIEW_DRAWER_MODE_REVIEW\)/) assert.match(createViewScriptSurface, /function switchReviewDrawerMode\(mode\) \{[\s\S]*if \(reviewDrawerMode\.value === mode\) \{[\s\S]*return[\s\S]*\}/) assert.match(createViewScriptSurface, /function switchToReviewOverviewDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_REVIEW\)/) assert.match(createViewScriptSurface, /function toggleReviewDocumentDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_DOCUMENTS\)/) assert.match(createViewScriptSurface, /function toggleReviewRiskDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_RISK\)/) assert.match(createViewScriptSurface, /function toggleReviewFlowDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_FLOW\)/) assert.doesNotMatch(createViewScriptSurface, /REVIEW_DRAWER_MODE_DOCUMENTS\s*\?\s*REVIEW_DRAWER_MODE_REVIEW/) assert.doesNotMatch(createViewScriptSurface, /REVIEW_DRAWER_MODE_RISK\s*\?\s*REVIEW_DRAWER_MODE_REVIEW/) assert.doesNotMatch(createViewScriptSurface, /REVIEW_DRAWER_MODE_FLOW\s*\?\s*REVIEW_DRAWER_MODE_REVIEW/) }) test('document review drawer fills sidebar height and preview dialog is centered', () => { assert.match(createViewTemplateSurface, /class="insight-body"[\s\S]*:class="\{ 'document-review-body': ui\.isReviewDocumentDrawer \}"/) assert.match(createViewBaseStyles, /\.insight-panel-shell\s*\{[\s\S]*display:\s*flex;[\s\S]*min-height:\s*0;/) assert.match(createViewPart2Styles, /\.insight-body\.document-review-body\s*\{[\s\S]*display:\s*flex;[\s\S]*overflow:\s*hidden;/) assert.match(createViewPart2Styles, /\.review-ticket-drawer\s*\{[\s\S]*grid-template-rows:\s*auto minmax\(0,\s*1fr\);[\s\S]*height:\s*100%;[\s\S]*overflow:\s*hidden;/) assert.match(createViewPart2Styles, /\.review-document-stage\s*\{[\s\S]*grid-template-rows:\s*auto auto minmax\(0,\s*1fr\);/) assert.match(createViewPart2Styles, /\.review-document-scroll\s*\{[\s\S]*max-height:\s*none;[\s\S]*min-height:\s*0;/) assert.match(createViewPart2Styles, /\.review-document-preview-card\.image\s*\{[\s\S]*place-items:\s*center;[\s\S]*min-height:\s*220px;/) assert.match(createViewPart2Styles, /\.review-document-preview-card\.image img\s*\{[\s\S]*height:\s*auto;[\s\S]*object-fit:\s*contain;/) assert.doesNotMatch(createViewPart2Styles, /\.review-document-preview-card\.image img\s*\{[\s\S]*object-fit:\s*cover;/) assert.match(createViewPart4Styles, /\.review-overlay\s*\{[\s\S]*align-items:\s*center;[\s\S]*justify-content:\s*center;/) assert.match(createViewPart4Styles, /\.review-preview-modal\s*\{[\s\S]*margin:\s*auto;[\s\S]*flex:\s*none;/) }) test('assistant conversation keeps composer visible when generated cards grow tall', () => { assert.match(createViewBaseStyles, /\.assistant-layout\s*\{[\s\S]*height:\s*100%;[\s\S]*max-height:\s*100%;[\s\S]*overflow:\s*hidden;/) assert.match( createViewBaseStyles, /\.dialog-panel\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-direction:\s*column;[\s\S]*height:\s*100%;[\s\S]*max-height:\s*100%;[\s\S]*min-height:\s*0;[\s\S]*overflow:\s*hidden;/ ) assert.match(createViewBaseStyles, /\.dialog-toolbar\s*\{[\s\S]*flex:\s*0 0 auto;/) assert.match( createViewBaseStyles, /\.message-list\s*\{[\s\S]*flex:\s*1 1 0;[\s\S]*min-height:\s*0;[\s\S]*max-height:\s*100%;[\s\S]*overflow-y:\s*auto;[\s\S]*overscroll-behavior:\s*contain;/ ) assert.match(createViewBaseStyles, /\.composer\s*\{[\s\S]*position:\s*sticky;[\s\S]*bottom:\s*0;[\s\S]*flex:\s*0 0 auto;[\s\S]*flex-shrink:\s*0;/) assert.match(createViewPart4Styles, /@media \(max-width:\s*1440px\)[\s\S]*\.dialog-panel\s*\{[\s\S]*flex:\s*1 1 0;[\s\S]*height:\s*auto;[\s\S]*max-height:\s*100%;/) }) test('document review OCR result card header keeps copy and navigation separated', () => { assert.match(insightPanelTemplate, /class="review-side-head-copy"[\s\S]*票据识别结果卡片[\s\S]*逐张查看 OCR 结果/) assert.match(insightPanelStyles, /\.review-document-switch-head\s*\{[\s\S]*display:\s*grid;[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\) auto;/) assert.match(insightPanelStyles, /\.review-side-head-copy\s*\{[\s\S]*min-width:\s*0;[\s\S]*display:\s*grid;/) assert.match(insightPanelStyles, /\.review-side-head-copy p\s*\{[\s\S]*overflow-wrap:\s*anywhere;/) assert.match(insightPanelStyles, /\.review-document-nav\s*\{[\s\S]*flex:\s*0 0 auto;[\s\S]*flex-wrap:\s*nowrap;[\s\S]*white-space:\s*nowrap;/) }) test('document preview avoids restored stale object urls', () => { assert.match(createViewTemplateSurface, /v-if="documentPreviewDialog\.kind === 'image'"[\s\S]*:key="documentPreviewDialog\.renderKey"/) assert.match(createViewTemplateSurface, /v-else-if="documentPreviewDialog\.kind === 'pdf'"[\s\S]*:key="documentPreviewDialog\.renderKey"/) assert.match(reviewDrawerScript, /renderKey:\s*''/) assert.match(reviewDrawerScript, /renderKey:\s*\[[\s\S]*Date\.now\(\)[\s\S]*\]\.join\('__'\)/) assert.match(attachmentsScript, /isTemporaryPreviewUrl/) assert.match(attachmentsScript, /existingPreview\?\.url && !isTemporaryPreviewUrl\(existingPreview\.url\)/) assert.match(sessionStateScript, /filterPersistableFilePreviews\(state\.reviewFilePreviews\)/) assert.doesNotMatch(sessionStateScript, /filterPersistableFilePreviews\(nextState\.reviewFilePreviews\)/) }) test('local transport review no longer uses the travel hotel template', () => { const reviewPayload = { slot_cards: [ { key: 'expense_type', label: '报销类型', value: '交通费', normalized_value: 'transport', status: 'identified' } ], document_cards: [ { document_type: 'taxi_receipt', suggested_expense_type: 'transport', scene_label: '交通费' } ] } assert.equal(isTravelReviewPayload(reviewPayload, { expense_type: '交通费' }), false) assert.match(reviewPanelModelScript, /shouldShowReviewFactCard\(reviewPayload, 'customer_name'/) assert.doesNotMatch( reviewPanelModelScript, /key:\s*'customer_name'[\s\S]{0,220}placeholder:\s*'请输入客户名称'[\s\S]{0,80}\},\s*\{[\s\S]{0,80}key:\s*'attachments'/ ) assert.match(createViewTemplateSurface, /placeholder="例如:出租车\/网约车票据 \/ 火车\/高铁票"/) assert.doesNotMatch(createViewTemplateSurface, /票据场景[\s\S]{0,260}例如:业务招待费 \/ 差旅费/) }) test('local save of changed reimbursement category updates edit fields too', () => { const nextPayload = buildLocallySyncedReviewPayload( { can_proceed: false, edit_fields: [ { key: 'expense_type', label: '报销分类', value: '交通费' }, { key: 'reason', label: '事由', value: '打车去客户现场' } ], slot_cards: [ { key: 'expense_type', label: '报销类型', value: '交通费', normalized_value: 'transport', required: true, status: 'identified' } ], confirmation_actions: [] }, { expense_type: '办公用品费', reason_value: '右侧核对后改为办公用品费' } ) const expenseTypeField = nextPayload.edit_fields.find((item) => item.key === 'expense_type') assert.equal(expenseTypeField.value, '办公用品费') assert.equal(nextPayload.slot_cards[0].value, '办公用品费') }) test('next step action uses rich text guidance and confirm dialog instead of footer button', () => { const reviewPayload = { can_proceed: true, risk_briefs: [ { level: 'low', title: '票据提示', content: '普通提示' } ], confirmation_actions: [ { action_type: 'save_draft', label: '保存为草稿' }, { action_type: 'next_step', label: '继续下一步', emphasis: 'primary' } ] } const copy = buildReviewNextStepRichCopy(reviewPayload, { detailHref: '/app/documents/claim-1' }) const rendered = renderMarkdown(copy) assert.match(copy, /系统识别您的单据已经填写完所有已知信息/) assert.match(copy, /现存在 1 条低风险,0 条中风险,0 条高风险/) assert.doesNotMatch(copy, /#review-risk-low/) assert.doesNotMatch(copy, /#review-risk-medium/) assert.doesNotMatch(copy, /#review-risk-high/) assert.match(copy, /\[右侧\]\(#review-risk-panel\) 风险信息提示窗/) assert.match(copy, /\[继续下一步\]\(#review-next-step\)/) assert.match(copy, /\[快速修改单据信息\]\(\/app\/documents\/claim-1\)/) assert.doesNotMatch(rendered, /markdown-risk-link-/) assert.match(rendered, /低风险<\/span>/) assert.match(rendered, /中风险<\/span>/) assert.match(rendered, /高风险<\/span>/) assert.doesNotMatch(rendered, /href="#review-risk-low"/) assert.doesNotMatch(rendered, /href="#review-risk-medium"/) assert.doesNotMatch(rendered, /href="#review-risk-high"/) assert.match(rendered, /markdown-action-link-risk/) assert.match(rendered, /markdown-action-link-next/) assert.deepEqual(resolveReviewFooterActions(reviewPayload), []) const highRiskCopy = buildReviewNextStepRichCopy( { ...reviewPayload, risk_briefs: [{ level: 'high', title: '金额超标' }] }, { detailHref: '/app/documents/claim-1' } ) assert.doesNotMatch(highRiskCopy, /\[继续下一步\]\(#review-next-step\)/) assert.match(createViewTemplateSurface, /class="review-next-step-rich-copy message-answer-markdown"[\s\S]*ui\.renderMarkdown\(ui\.buildReviewNextStepRichCopyForMessage\(message\)\)/) assert.match(createViewTemplateSurface, /class="message-bubble"[\s\S]*:class="ui\.buildMessageBubbleClass\(message\)"/) assert.match(createViewTemplateSurface, /:open="nextStepConfirmDialog\.open"[\s\S]*title="确认提交当前单据?"[\s\S]*confirm-text="确认提交"/) assert.match(createViewScriptSurface, /const REVIEW_NEXT_STEP_HREF = '#review-next-step'/) assert.match(createViewScriptSurface, /buildReviewRiskLevelCounts/) assert.match(createViewScriptSurface, /function buildMessageBubbleClass\(message\)/) assert.match(createViewScriptSurface, /message-bubble-review-risk-high/) assert.match(createViewScriptSurface, /message-bubble-review-risk-medium/) assert.match(createViewScriptSurface, /message-bubble-review-risk-low/) assert.match(createViewScriptSurface, /function openReviewNextStepConfirm\(message\)/) assert.match(createViewScriptSurface, /async function confirmReviewNextStepSubmit\(\)/) assert.match(createViewScriptSurface, /href === REVIEW_NEXT_STEP_HREF[\s\S]*openReviewNextStepConfirm\(message\)/) assert.match(createViewScriptSurface, /href\.startsWith\(REVIEW_RISK_PANEL_HREF_PREFIX\)[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_RISK\)/) assert.match(createViewBaseStyles, /\.message-bubble-review-risk-low\s*\{[\s\S]*border-color:\s*rgba\(37,\s*99,\s*235,/) assert.match(createViewBaseStyles, /\.message-bubble-review-risk-medium\s*\{[\s\S]*border-color:\s*rgba\(217,\s*119,\s*6,/) assert.match(createViewBaseStyles, /\.message-bubble-review-risk-high\s*\{[\s\S]*border-color:\s*rgba\(220,\s*38,\s*38,/) assert.doesNotMatch(createViewBaseStyles, /markdown-risk-link-low/) assert.match(createViewBaseStyles, /\.markdown-risk-text-low[\s\S]*color:\s*#2563eb/) assert.match(createViewBaseStyles, /\.markdown-risk-text-medium[\s\S]*color:\s*#d97706/) assert.match(createViewBaseStyles, /\.markdown-risk-text-high[\s\S]*color:\s*#dc2626/) assert.match(createViewPart3Styles, /\.review-next-step-rich-copy\s*\{[\s\S]*margin-top:\s*30px;/) }) test('review risk drawer lists risk briefs without score and posts details into the conversation', () => { const riskItemsBlock = reviewPanelModelScript.match(/function buildReviewRiskItems\(reviewPayload\) \{[\s\S]*?\n\}\n\nexport function buildReviewRiskConversationText/) assert.ok(riskItemsBlock, 'risk item builder should be present') assert.doesNotMatch(createViewTemplateSurface, /review-side-risk-score/) assert.doesNotMatch(createViewTemplateSurface, /风险评分/) assert.doesNotMatch(createViewTemplateSurface, /暂无风险评分/) assert.doesNotMatch(createViewScriptSurface, /function buildReviewRiskScore/) assert.doesNotMatch(createViewScriptSurface, /const reviewRiskScore/) assert.doesNotMatch(riskItemsBlock[0], /\.slice\(0,\s*6\)/) assert.match(reviewPanelModelScript, /const DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS = \[[\s\S]*'历史报销画像'[\s\S]*'制度注意事项'/) assert.match( reviewPanelModelScript, /function resolveReviewRiskBriefs\(reviewPayload\) \{[\s\S]*DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS\.some/ ) assert.match( createViewTemplateSurface, /class="review-side-risk-item"[\s\S]*@click="ui\.appendReviewRiskBriefToConversation\(item\)"/ ) assert.doesNotMatch(createViewTemplateSurface, /\{\{\s*item\.levelLabel\s*\}\}/) assert.match(createViewTemplateSurface, /class="review-side-risk-icon" :title="item\.levelLabel"/) assert.match(reviewPanelModelScript, /info:\s*\{[\s\S]*label:\s*'提示'/) assert.match(reviewPanelModelScript, /medium:\s*\{[\s\S]*label:\s*'中风险'/) assert.match(reviewPanelModelScript, /low:\s*\{[\s\S]*label:\s*'低风险'/) assert.match(reviewPanelModelScript, /const isInfo = String\(item\?\.level \|\| ''\)\.trim\(\) === 'info'/) assert.match(reviewPanelModelScript, /\$\{isInfo \? '提示内容' : '风险点'\}:\$\{summary\}/) assert.match(reviewPanelModelScript, /\$\{isInfo \? '处理建议' : '修改建议'\}:\$\{suggestion\}/) assert.match(reviewPanelModelScript, /function normalizeReviewRiskTitle/) assert.match(reviewPanelModelScript, /\.replace\(\/AI\\s\*预审/) assert.match(reviewPanelModelScript, /\.replace\(\/\(高风险\|中风险\|低风险\)\/g,\s*''\)/) assert.match(reviewPanelModelScript, /sourceLabel:\s*meta\.label/) assert.doesNotMatch(reviewPanelModelScript, /normalizedTitle\.includes\('AI预审'\)/) assert.match(createViewScriptSurface, /metaTone:\s*item\.level \|\| 'low'/) assert.doesNotMatch(createViewTemplateSurface, /@click="openReviewRiskDetail\(item\)"/) assert.doesNotMatch(createViewTemplateSurface, /review-risk-detail-modal/) assert.doesNotMatch(createViewScriptSurface, /reviewRiskDetailDialog/) assert.doesNotMatch(createViewScriptSurface, /function openReviewRiskDetail/) assert.match( createViewScriptSurface, /function appendReviewRiskBriefToConversation\(item\) \{[\s\S]*messages\.value\.push\(createMessage\('assistant'/ ) assert.match(reviewPanelModelScript, /function buildReviewRiskConversationText\(item, detailTarget = \{\}\)/) assert.match(createViewScriptSurface, /function resolveReviewDetailTarget\(message = null\) \{[\s\S]*router\.resolve\(\{[\s\S]*name: 'app-document-detail'/) assert.match(createViewScriptSurface, /function resolveReviewRiskDetailTarget\(\) \{[\s\S]*return resolveReviewDetailTarget\(\)/) assert.match(createViewScriptSurface, /进入 \$\{claimNo\} 详情重新填写/) assert.match(createViewTemplateSurface, /class="expense-query-risk-row"[\s\S]*ui\.appendExpenseQueryRiskToConversation\(record, risk\)/) assert.match(createViewScriptSurface, /function appendExpenseQueryRiskToConversation\(record, risk\) \{[\s\S]*进入 \$\{claimNo\} 详情重新填写/) }) test('review drawer default mode is scoped by the current action and travel overview uses travel-specific fields', () => { assert.match(reviewDrawerScript, /activeReviewPanelScope/) assert.match(reviewDrawerScript, /const reviewOverviewDrawerAvailable = computed\(\(\) => normalizedReviewPanelScope\.value === 'overview'\)/) assert.match(reviewDrawerScript, /scope === 'documents' && hasDocuments[\s\S]*REVIEW_DRAWER_MODE_DOCUMENTS/) assert.match(reviewDrawerScript, /scope === 'risk' && hasRisks[\s\S]*REVIEW_DRAWER_MODE_RISK/) assert.match(reviewDrawerScript, /scope === 'overview'[\s\S]*REVIEW_DRAWER_MODE_REVIEW/) assert.match(reviewPanelModelScript, /function normalizeReviewPanelScope\(scope\)/) assert.match(createViewScriptSurface, /canExposeReviewPanelScope\(item\.reviewPanelScope\)/) assert.match(createViewScriptSurface, /currentInsight\.value\.intent === 'agent' && agent[\s\S]*return null/) assert.match(reviewPanelModelScript, /function isTravelReviewPayload\(reviewPayload/) assert.match(reviewPanelModelScript, /function resolveReviewTravelTransportType\(reviewPayload/) assert.match(reviewPanelModelScript, /label: '交通类型'[\s\S]*modelKey: 'transport_type'/) assert.match(reviewPanelModelScript, /label: '酒店名称'[\s\S]*modelKey: 'merchant_name'/) assert.match(reviewPanelModelScript, /label: '出差事宜'[\s\S]*editor: 'textarea'[\s\S]*wide: true/) assert.match(createViewTemplateSurface, /item\.editor === 'textarea'[\s\S]*