P0-2: - content-adapter.js: 解耦Node crypto(客户端直引), 新增mergeCmsItem/normalizeThumbnail/buildPublishPayload - buildPublishPayload补normalizeCmsList归一(抓到vote_id漏生成真bug) - check-article-flow.js: 4组->13组断言,覆盖a-g P1-2: - editText.vue/smallTitle.vue getContents成功回调加空覆盖守卫 - editSetting封面已统一字符串数组,无需改动 验证: verify.js全绿,h5构建90 JS通过
170 lines
8.4 KiB
JavaScript
170 lines
8.4 KiB
JavaScript
#!/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: '<p>首段</p>', delta: { ops: [{ insert: '首段' }] } }
|
||
const merged = mergeCmsItem(oldItem, { text: '首段', html: '<p>首段</p>', delta: { ops: [{ insert: '首段' }] } })
|
||
assert.ok(merged, '同内容写回应合并成功')
|
||
assert.strictEqual(merged.text, '首段')
|
||
assert.strictEqual(merged.type, 'text')
|
||
})
|
||
group('a2 首块改后新文落盘', () => {
|
||
const oldItem = { type: 'text', text: '首段', html: '<p>首段</p>', delta: { ops: [{ insert: '首段' }] } }
|
||
const merged = mergeCmsItem(oldItem, { text: '改后', html: '<p>改后</p>', 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: '<p>旧文</p>', 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: '<p>小标题</p>', 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: '<p>小标题</p>', 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 }
|