'use strict'; /** * 「前台投稿」后台设置页契约(对齐参考主题 1:1,不含专题): * 1) PHP schema(SchemaPostList.php submit section)字段键、顺序、默认值与 * JS schema(assets/admin/src/schema/sections/post-list/submit.js)一致; * 2) 参考主题字段全集存在:post_article_s / post_article_btn_txte / post_article_cat / * post_article_custom_filter_s / post_article_tag / post_article_user / * post_article_title_strlen_limit / post_article_pay_free_s / * post_article_pay_price_limit / post_article_pay_vip_price_s / * post_article_pay_vip_1_discount / post_article_pay_vip_2_discount / * post_t_placeholder / post_c_placeholder; * 3) 专题(topics)相关字段不存在; * 4) 旧 AKSRT 私有字段(front_submit_*、post_article_title_strlen_limit_min/max、 * post_article_pay_price_min/max)已彻底移除,无双路兼容; * 5) 运行时(PostDisplay::get_submit_settings)只读取新键。 */ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const php = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaPostList.php'), 'utf8'); const js = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/post-list/submit.js'), 'utf8'); const runtime = fs.readFileSync(path.join(root, 'src/Settings/PostDisplay.php'), 'utf8'); const REFERENCE_KEYS = [ 'post_article_s', 'post_article_btn_txte', 'post_article_cat', 'post_article_custom_filter_s', 'post_article_tag', 'post_article_user', 'post_article_title_strlen_limit', 'post_article_pay_free_s', 'post_article_pay_price_limit', 'post_article_pay_vip_price_s', 'post_article_pay_vip_1_discount', 'post_article_pay_vip_2_discount', 'post_t_placeholder', 'post_c_placeholder', ]; // PHP 源码中字段定义的完整出现顺序(含 post_article_pay_price_limit 的 // fieldset 嵌套子字段 price / points)。 const PHP_FIELD_ORDER = [ ...REFERENCE_KEYS.slice(0, REFERENCE_KEYS.indexOf('post_article_pay_vip_price_s')), 'price', 'points', ...REFERENCE_KEYS.slice(REFERENCE_KEYS.indexOf('post_article_pay_vip_price_s')), ]; const LEGACY_KEYS = [ 'front_submit_enabled', 'front_submit_roles', 'front_submit_title', 'front_submit_content', 'front_submit_excerpt', 'front_submit_cover', 'front_submit_category', 'front_submit_guest_enabled', 'front_submit_tags', 'front_submit_custom_fields', 'front_submit_review_mode', 'front_submit_cover_required', 'front_submit_max_images', 'front_submit_editor_mode', 'front_submit_draft_save', 'front_submit_leave_warning', 'post_article_title_strlen_limit_min', 'post_article_title_strlen_limit_max', 'post_article_pay_price_min', 'post_article_pay_price_max', ]; /** Extract the PHP 'submit' => [ ... ] section body (balanced brackets). */ function phpSubmitSection(src) { const start = src.indexOf("'submit' => ["); assert.notEqual(start, -1, 'PHP schema has a submit section'); const open = src.indexOf('[', start); let depth = 0; for (let i = open; i < src.length; i++) { const ch = src[i]; if (ch === "'" || ch === '"') { const quote = ch; i++; while (i < src.length) { if (src[i] === '\\') { i += 2; continue; } if (src[i] === quote) break; i++; } continue; } if (ch === '[') depth++; else if (ch === ']') { depth--; if (depth === 0) return src.slice(open, i + 1); } } throw new Error('unbalanced submit section'); } const submitPhp = phpSubmitSection(php); test('PHP submit section 字段键与参考主题一致且顺序相同', () => { // 字段定义均为 ['key' => '...', 'label' => ...] 开头;dependsOn 内的 // ['key' => '...', 'value' => ...] 不是字段定义,须排除。 const found = []; const re = /'key'\s*=>\s*'([a-z0-9_]+)',\s*'label'/g; let m; while ((m = re.exec(submitPhp)) !== null) found.push(m[1]); assert.deepEqual(found, PHP_FIELD_ORDER); }); test('PHP submit section 不含专题字段', () => { assert.doesNotMatch(submitPhp, /post_article_topics/); assert.doesNotMatch(submitPhp, /'topics'/); }); test('PHP submit section 默认值对齐参考主题', () => { assert.match(submitPhp, /'post_article_s',\s*'label'\s*=>\s*'启用前台投稿',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/); assert.match(submitPhp, /'post_article_btn_txte',\s*'label'\s*=>\s*'按钮文字',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'发布文章'/); assert.match(submitPhp, /'post_article_user',\s*'label'\s*=>\s*'免登陆投稿发布用户',\s*'type'\s*=>\s*'user_select',\s*'default'\s*=>\s*1/); assert.match(submitPhp, /'post_article_title_strlen_limit',\s*'label'\s*=>\s*'标题字数限制',\s*'type'\s*=>\s*'between_number',\s*'default'\s*=>\s*\['min'\s*=>\s*5,\s*'max'\s*=>\s*30\]/); assert.match(submitPhp, /'post_article_pay_vip_1_discount',\s*'label'\s*=>\s*'一级会员折扣',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*100/); assert.match(submitPhp, /'post_article_pay_vip_2_discount',\s*'label'\s*=>\s*'二级会员折扣',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*100/); assert.match(submitPhp, /'post_t_placeholder',\s*'label'\s*=>\s*'投稿标题占位符',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'请输入标题'/); assert.match(submitPhp, /'post_c_placeholder',\s*'label'\s*=>\s*'投稿内容占位符',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'请输入内容'/); assert.match(submitPhp, /'price',\s*'label'\s*=>\s*'现金金额限制'/); assert.match(submitPhp, /'points',\s*'label'\s*=>\s*'积分金额限制'/); }); test('会员折扣仅在关闭手动会员价时显示(对齐参考主题 dependency)', () => { assert.match( submitPhp, /'post_article_pay_vip_1_discount'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'post_article_s',\s*'value'\s*=>\s*true\],\s*\['key'\s*=>\s*'post_article_pay_vip_price_s',\s*'value'\s*=>\s*false\]\]\]/ ); assert.match( submitPhp, /'post_article_pay_vip_2_discount'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'post_article_s',\s*'value'\s*=>\s*true\],\s*\['key'\s*=>\s*'post_article_pay_vip_price_s',\s*'value'\s*=>\s*false\]\]\]/ ); }); test('JS schema 与 PHP 字段键一致(ui_only content 行除外)', () => { // 字段定义为 { key: '...', label: ... };dependsOn 内的 { key, value } 须排除。 const keys = []; const re = /key:\s*'([a-z0-9_]+)',\s*label:/g; let m; while ((m = re.exec(js)) !== null) keys.push(m[1]); const persisted = keys.filter((k) => REFERENCE_KEYS.includes(k)); assert.deepEqual(persisted, REFERENCE_KEYS); for (const legacy of LEGACY_KEYS) { assert.ok(!keys.includes(legacy), `JS schema 残留旧字段: ${legacy}`); } // 两个 ui_only 提示行承载参考主题 desc 中的【去设置】链接。 assert.match(js, /permissions\/capabilities/); assert.match(js, /post-list\/filter/); assert.match(js, /ui_only:\s*true/); }); test('JS schema 默认值对齐参考主题', () => { assert.match(js, /post_article_user:\s*1\b/); assert.match(js, /post_article_pay_vip_1_discount:\s*100\b/); assert.match(js, /post_article_pay_vip_2_discount:\s*100\b/); assert.match(js, /post_t_placeholder:\s*'请输入标题'/); assert.match(js, /post_c_placeholder:\s*'请输入内容'/); assert.match(js, /price:\s*\{\s*min:\s*0,\s*max:\s*99999\s*\}/); }); test('旧字段在 schema 与运行时中均已移除(无双路)', () => { for (const legacy of LEGACY_KEYS) { assert.ok(!php.includes(legacy), `PHP schema 残留旧字段: ${legacy}`); assert.ok(!runtime.includes(legacy), `PostDisplay 残留旧字段: ${legacy}`); } // 运行时读取新键。 assert.match(runtime, /\$settings\['post_article_s'\]/); assert.match(runtime, /\$settings\['post_article_title_strlen_limit'\]/); assert.match(runtime, /\$settings\['post_article_pay_price_limit'\]/); assert.match(runtime, /\$settings\['post_article_tag'\]/); assert.match(runtime, /\$settings\['post_article_user'\]/); });