175 lines
8.2 KiB
JavaScript
175 lines
8.2 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const source = (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
|
|
|
test('工作上报首屏是语音优先会话,不再暴露传统表单主路径', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
|
|
assert.match(page, /成果投稿/);
|
|
assert.match(page, /把好经验沉淀给公司复用/);
|
|
assert.match(page, /我的投稿/);
|
|
assert.match(page, /哪段处置经验值得公司复用?按住说,也可以发照片或视频,我会整理成案例、SOP 或知识投稿。/);
|
|
assert.match(page, /<ChatComposer/);
|
|
assert.doesNotMatch(page, /选择上报类型/);
|
|
assert.doesNotMatch(page, /class="type-grid"/);
|
|
assert.doesNotMatch(page, /placeholder="一句话说清上报内容"/);
|
|
});
|
|
|
|
test('工作上报为 ChatComposer 提供自己的照片视频和音频面板', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
|
|
assert.match(page, /:panel-items="composerPanelItems"/);
|
|
assert.match(page, /label: '照片\/视频'/);
|
|
assert.match(page, /label: '选择音频'/);
|
|
assert.match(page, /@panel="handlePanelAction"/);
|
|
assert.doesNotMatch(page, /仅用于本次现场分析/);
|
|
});
|
|
|
|
test('语音转写、文字和附件补充都进入无状态整理', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
|
|
assert.match(page, /语音转文字/);
|
|
assert.match(page, /transcribeSpeechCapture/);
|
|
assert.match(page, /transcribeSpeechFile/);
|
|
assert.match(page, /organizeWorkReport\(/);
|
|
assert.match(page, /const previousDraft = draftPayload\(\)/);
|
|
assert.match(page, /currentDraft:\s*previousDraft/);
|
|
assert.match(page, /attachmentOssId:\s*attachment\.value\?\.ossId/);
|
|
assert.match(page, /void organizeInput\(text, \{ voice: false \}\)/);
|
|
assert.match(page, /void addMediaAttachment\(\)/);
|
|
});
|
|
|
|
test('短追问最多两条,文字直接可见并只提供播放', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
|
|
assert.match(page, /missingQuestions\.slice\(0, 2\)/);
|
|
assert.match(page, /class="question-text"/);
|
|
assert.match(page, /toggleQuestionSpeech/);
|
|
assert.match(page, /aria-label="播放追问"/);
|
|
assert.doesNotMatch(page, />转文字<\/button>/);
|
|
assert.doesNotMatch(page, /收起文字/);
|
|
});
|
|
|
|
test('确认卡允许修改最终类型标题正文且不承诺自动脱敏', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
|
|
assert.match(page, /建议类型/);
|
|
assert.match(page, /changeDraftType/);
|
|
assert.match(page, /v-model="currentDraft\.title"/);
|
|
assert.match(page, /v-model="currentDraft\.content"/);
|
|
assert.match(page, /检测到具体房号,请修改后再提交/);
|
|
assert.match(page, /const hasRoomPrivacyWarning = computed/);
|
|
assert.match(page, /请处理上述敏感信息后再提交/);
|
|
assert.match(page, /const finishDraftEditing = async/);
|
|
assert.match(page, /manuallyConfirmedType\.value\s*\|\|/);
|
|
assert.match(page, /manuallyConfirmedType\.value = selected\.value/);
|
|
assert.match(page, /requestId:\s*submitRequestId\.value[\s\S]*type:\s*draft\.suggestedType/);
|
|
assert.match(page, /transcript:\s*''/);
|
|
assert.match(page, /if \(privacyReviewRequired\.value\)/);
|
|
assert.doesNotMatch(page, /privacyReviewed/);
|
|
assert.match(page, /editingDraft \? '完成修改' : '修改'/);
|
|
assert.match(page, /@click="confirmSubmit"/);
|
|
assert.match(page, /'确认投稿'/);
|
|
assert.doesNotMatch(page, /自动隐藏/);
|
|
assert.doesNotMatch(page, />继续补充<\/button>/);
|
|
});
|
|
|
|
test('正式提交只发生在确认动作并复用稳定幂等键', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
const submitCalls = page.match(/submitWorkReport\(/g) || [];
|
|
|
|
assert.equal(submitCalls.length, 1);
|
|
assert.match(page, /const confirmSubmit = async \(\) =>/);
|
|
assert.match(page, /if \(submitActionDisabled\.value \|\| editingDraft\.value\)/);
|
|
assert.match(page, /stableWorkReportRequestId\(submitRequestId\.value, createRequestId\)/);
|
|
assert.match(page, /requestId:\s*submitRequestId\.value/);
|
|
assert.match(page, /const draftLocked = computed/);
|
|
assert.match(page, /\|\| Boolean\(submitRequestId\.value\)/);
|
|
assert.match(page, /isDefinitiveWorkReportRejection/);
|
|
assert.match(page, /if \(rejected\) submitRequestId\.value = ''/);
|
|
assert.match(page, /const record = await submitWorkReport/);
|
|
assert.match(page, /submittedReport\.value = record/);
|
|
assert.match(page, /await loadMine\(true\)/);
|
|
assert.match(page, /PENDING/);
|
|
assert.match(page, /待审核/);
|
|
assert.match(page, /submittedReport\.status\.toLowerCase\(\)/);
|
|
assert.match(page, /submittedReport\.statusLabel/);
|
|
assert.match(page, /@click="startNewReport">再报一条/);
|
|
});
|
|
|
|
test('异步请求具备 generation、上传取消和离页 teardown', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
|
|
assert.match(page, /let organizeGeneration = 0/);
|
|
assert.match(page, /isCurrentWorkReportRequest/);
|
|
assert.match(page, /currentAccountKey\(\)/);
|
|
assert.match(page, /visibleAccountKey/);
|
|
assert.match(page, /mediaSelecting\.value/);
|
|
assert.match(page, /abortVoiceTranscription\?\.\(\)/);
|
|
assert.match(page, /activeUploadTask\?\.abort\(\)/);
|
|
assert.match(page, /!previousDraft \|\| submitActionDisabled\.value \|\| draftLocked\.value/);
|
|
assert.match(page, /:disabled="draftLocked \|\| submitActionDisabled"/);
|
|
assert.match(page, /composerRef\.value\?\.teardown\(\)/);
|
|
assert.match(page, /speechPlayback\.stop\(\)/);
|
|
assert.match(page, /onHide\(teardown\)/);
|
|
assert.match(page, /onUnload/);
|
|
});
|
|
|
|
test('AI 长草稿不被前端静默截断,改由确认时明确校验', async () => {
|
|
const page = await source('../src/pages/user/report/index.vue');
|
|
|
|
assert.doesNotMatch(page, /String\(result\.content[\s\S]*?\.slice\(0, 2000\)/);
|
|
assert.match(page, /content\.length > 2000/);
|
|
assert.match(page, /投稿内容不能超过2000字/);
|
|
});
|
|
|
|
test('工作上报服务声明整理契约并在正式提交携带 requestId', async () => {
|
|
const service = await source('../src/services/work-report.ts');
|
|
|
|
assert.match(service, /export interface WorkReportDraft/);
|
|
assert.match(service, /suggestedType:\s*WorkReportType/);
|
|
assert.match(service, /export interface OrganizeResponse extends WorkReportDraft/);
|
|
assert.match(service, /missingQuestions:\s*string\[\]/);
|
|
assert.match(service, /privacyWarnings:\s*string\[\]/);
|
|
assert.match(service, /export const organizeWorkReport/);
|
|
assert.match(service, /\/api\/aihr\/work-report\/organize/);
|
|
assert.match(service, /requestId:\s*string/);
|
|
assert.match(service, /data:\s*params/);
|
|
});
|
|
|
|
test('幂等键与异步账号守卫是可执行行为,不只是源码字符串', async (context) => {
|
|
const { createServer } = await import('vite');
|
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
const server = await createServer({
|
|
root,
|
|
configFile: false,
|
|
appType: 'custom',
|
|
logLevel: 'silent',
|
|
resolve: { alias: { '@': fileURLToPath(new URL('../src', import.meta.url)) } },
|
|
server: { middlewareMode: true }
|
|
});
|
|
context.after(() => server.close());
|
|
const state = await server.ssrLoadModule('/src/services/work-report.ts');
|
|
|
|
let created = 0;
|
|
const create = () => `request-${++created}`;
|
|
const first = state.stableWorkReportRequestId('', create);
|
|
const retry = state.stableWorkReportRequestId(first, create);
|
|
assert.equal(first, retry);
|
|
assert.equal(created, 1);
|
|
|
|
assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 400 }), true);
|
|
assert.equal(state.isDefinitiveWorkReportRejection({ bizCode: 409 }), false);
|
|
assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 408 }), false);
|
|
assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 429 }), false);
|
|
assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 500 }), false);
|
|
assert.equal(state.isDefinitiveWorkReportRejection({}), false);
|
|
|
|
assert.equal(state.isCurrentWorkReportRequest(3, 3, 'account-a', 'account-a'), true);
|
|
assert.equal(state.isCurrentWorkReportRequest(2, 3, 'account-a', 'account-a'), false);
|
|
assert.equal(state.isCurrentWorkReportRequest(3, 3, 'account-a', 'account-b'), false);
|
|
});
|