#!/usr/bin/env node 'use strict' /** * 文章内容适配契约测试(P0-2)。 * * 覆盖 NEXT-STEPS.md B/P0-2 的 a–g(共 13 组断言组): * a. 编辑首个文字块(index=0)不被清空 → mergeCmsItem * b. 视频改说明后 src/poster/duration 仍在 → normalizeCmsList * c. 小标题能写入 cmsLst(type:'title') → normalizeCmsList/cmsLstToDelta * d. 封面始终是字符串数组 → normalizeThumbnail * e. mobile 发布 payload 同时含 cmsLst + content → buildPublishPayload * f. 5 种块在预览/详情分流后都有渲染目标 → cmsLstToDelta/getArticleText/getArticleImages * g. hasRenderableContent 对空/纯空块的判定 → hasRenderableContent * * 运行:node tools/check-article-flow.js */ const assert = require('assert') const { normalizeCmsList, cmsLstToDelta, createVoteId, getArticleText, getArticleImages, hasRenderableContent, mergeCmsItem, normalizeThumbnail, buildPublishPayload } = require('../uniCloud-alipay/cloudfunctions/uni-cms-articles/content-adapter') let groups = 0 function group(name, fn) { groups++ fn() console.log(` [${groups}] ${name}`) } function run() { console.log('文章内容适配契约(a–g):') // —— a. 首块 index=0 不被清空(editText.vue:97-108 同源语义)—— group('a1 首块原文写回保留', () => { const oldItem = { type: 'text', text: '首段', html: '

首段

', delta: { ops: [{ insert: '首段' }] } } const merged = mergeCmsItem(oldItem, { text: '首段', html: '

首段

', delta: { ops: [{ insert: '首段' }] } }) assert.ok(merged, '同内容写回应合并成功') assert.strictEqual(merged.text, '首段') assert.strictEqual(merged.type, 'text') }) group('a2 首块改后新文落盘', () => { const oldItem = { type: 'text', text: '首段', html: '

首段

', delta: { ops: [{ insert: '首段' }] } } const merged = mergeCmsItem(oldItem, { text: '改后', html: '

改后

', delta: { ops: [{ insert: '改后' }] } }) assert.ok(merged) assert.strictEqual(merged.text, '改后') }) // —— P1-2 空覆盖守卫(editText/smallTitle getContents 成功回调同源语义)—— group('P1-2 有旧内容+空新内容→拒绝覆盖(null)', () => { const oldItem = { type: 'text', text: '旧文', html: '

旧文

', delta: { ops: [{ insert: '旧文' }] } } assert.strictEqual(mergeCmsItem(oldItem, { text: '', html: '', delta: { ops: [] } }), null) assert.strictEqual(mergeCmsItem(oldItem, { text: '', html: '' }), null) }) group('P1-2 新建空块允许落盘(无旧内容不拦截)', () => { const merged = mergeCmsItem(undefined, { text: '', html: '', delta: { ops: [] } }) assert.ok(merged && merged.text === '') }) // —— b. 视频改说明保留媒体字段(editCms.vue video 分支同源语义)—— group('b 视频改说明后 src/poster/duration 仍在', () => { const list = normalizeCmsList([ { type: 'video', text: '新说明', video: { src: 'cloud://video-a', poster: 'cloud://poster-a', duration: 12 } } ]) assert.strictEqual(list.length, 1) assert.strictEqual(list[0].video.src, 'cloud://video-a') assert.strictEqual(list[0].video.poster, 'cloud://poster-a') assert.strictEqual(list[0].video.duration, 12) assert.strictEqual(list[0].text, '新说明') }) // —— c. 小标题写入(smallTitle.vue:120-140 同源语义)—— group('c 小标题 type:title 进 cmsLst 且进 Delta', () => { const list = normalizeCmsList([ { type: 'title', text: '小标题', html: '

小标题

', delta: { ops: [{ insert: '小标题' }] } } ]) assert.strictEqual(list.length, 1) assert.strictEqual(list[0].type, 'title') const delta = cmsLstToDelta(list) assert.ok(delta.ops.some(op => op.insert === '小标题'), 'Delta 含小标题文本') }) // —— d. 封面字符串数组(editSetting.vue 同源语义)—— group('d1 三图/单图/无封面归一', () => { assert.deepStrictEqual(normalizeThumbnail(['a', 'b', 'c'], 3), ['a', 'b', 'c']) assert.deepStrictEqual(normalizeThumbnail(['only'], 1), ['only']) assert.deepStrictEqual(normalizeThumbnail(['a', 'b'], 0), []) }) group('d2 空串与对象形态被过滤/抽取', () => { assert.deepStrictEqual(normalizeThumbnail(['', 'b', ''], 3), ['b']) assert.deepStrictEqual(normalizeThumbnail([{ src: 'x' }, { src: '' }], 3), ['x']) assert.deepStrictEqual(normalizeThumbnail([''], 1), []) }) // —— e. 发布 payload(preview.vue:404-420 同源语义)—— group('e1 发布同时含 cmsLst+content 且 vote_id 非空', () => { const cmsLst = [ { type: 'text', text: '首段', html: '', delta: { ops: [{ insert: '首段' }] } }, { type: 'vote', vote: { voteTitle: '投票', voteLst: [{ value: 'A' }, { value: 'B' }] } } ] const r = buildPublishPayload({ title: '标题', cmsLst }, { articleStatus: 1 }) assert.ok(!r.error, r.error || '发布应成功') assert.ok(Array.isArray(r.query.cmsLst) && r.query.cmsLst.length === 2) assert.ok(r.query.content && Array.isArray(r.query.content.ops) && r.query.content.ops.length > 0) assert.match(r.query.cmsLst[1].vote.vote_id, /^vote_/) assert.strictEqual(r.query.edit_type, 'mobile') assert.strictEqual(r.query.article_status, 1) }) group('e2 无标题/空正文发布被拦,草稿只拦标题', () => { const cmsLst = [{ type: 'text', text: '首段', html: '', delta: { ops: [{ insert: '首段' }] } }] assert.strictEqual(buildPublishPayload({ title: ' ', cmsLst }, { articleStatus: 1 }).error, '请先填写标题') assert.strictEqual(buildPublishPayload({ title: '标题', cmsLst: [] }, { articleStatus: 1 }).error, '正文不能为空') const draft = buildPublishPayload({ title: '标题', cmsLst: [] }, { articleStatus: 0 }) assert.ok(!draft.error, '草稿允许空正文') assert.strictEqual(draft.query.article_status, 0) }) // —— f. 5 种块渲染分流 —— group('f 5种块各有 Delta/文本/图片渲染目标', () => { const list = normalizeCmsList([ { type: 'text', text: '首段', delta: { ops: [{ insert: '首段' }, { insert: '\n' }] } }, { type: 'title', text: '小标题', html: '

小标题

', delta: { ops: [{ insert: '小标题' }] } }, { type: 'image', image: { src: 'cloud://image-a' } }, { type: 'video', text: '视频说明', video: { src: 'cloud://video-a', poster: 'cloud://poster-a', duration: 12 } }, { type: 'vote', vote: { voteTitle: '投票', voteLst: [{ value: 'A' }, { value: 'B' }] } }, { type: 'text', text: '' }, { type: 'image', image: { src: '' } } ]) assert.strictEqual(list.length, 5, '空块被过滤后剩 5 块') const delta = cmsLstToDelta(list) assert.ok(delta.ops.some(op => op.insert && op.insert.image === 'cloud://image-a'), '图片有渲染目标') assert.ok(delta.ops.some(op => op.insert && op.insert.video === 'cloud://video-a'), '视频有渲染目标') assert.ok(delta.ops.some(op => op.insert === '投票\n'), '投票标题有渲染目标') const text = getArticleText(delta, list) assert.match(text, /首段/) assert.match(text, /小标题/) assert.match(text, /视频说明/) assert.deepStrictEqual(getArticleImages(delta, list).sort(), ['cloud://image-a', 'cloud://poster-a']) }) // —— g. hasRenderableContent 空判定 —— group('g 空与纯空块判 false,有内容判 true', () => { assert.strictEqual(hasRenderableContent({ ops: [] }, []), false) assert.strictEqual(hasRenderableContent(null, [{ type: 'text', text: '' }]), false) assert.strictEqual(hasRenderableContent(null, [{ type: 'image', image: { src: '' } }]), false) assert.strictEqual(hasRenderableContent({ ops: [{ insert: 'x' }] }, []), true) assert.strictEqual(hasRenderableContent(null, [{ type: 'text', text: '有' }]), true) assert.strictEqual(hasRenderableContent(null, [{ type: 'vote', vote: { voteTitle: '投', voteLst: [{ value: 'A' }, { value: 'B' }] } }]), true) }) group('基础 vote_id 唯一且前缀正确', () => { assert.match(createVoteId(), /^vote_/) assert.notStrictEqual(createVoteId(), createVoteId()) }) assert.ok(groups >= 10, `断言组数 ${groups} < 10`) console.log(`✓ 文章内容适配自测通过(${groups} 组断言,覆盖 a–g)`) } if (require.main === module) run() module.exports = { run }