Files
wordpress-theme-aksrt/tests/widget-system/front-submit-editor-contract.test.cjs
kate522 d4bb43a275 feat(attachment): 按参考主题重构后台上传配置套件
- 新增 AKSRT\Attachment\UploadLimits:按用户类型解析图片/视频/文件
  上传上限(取最大值+默认兜底,钳制到 PHP 上限)、允许/禁止格式过滤、
  上传自动重命名、非管理员附件页 404;不含论坛角色
- 后台「附件配置」重做:大小限制/格式限制/上传行为三组配置,
  PHP Schema 与 SPA Schema 逐字段镜像(v1.2.0)
- 定制页 AttachmentPermissions.vue 改为标准 schemaPage,清理失效的
  API 包装、组件加载器与旧配置键(submit_image_max_size 等)
- 投稿/评论上传、批量上传数量全部切换到新链路;AksField 数字组件
  支持单位展示
- 更新 captcha harness 与 front-submit 契约测试;421 项测试通过
2026-09-24 11:04:49 +08:00

143 lines
7.2 KiB
JavaScript

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 read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('front submit enqueues the WordPress editor before wp_head is rendered', () => {
const source = read('templates/pages/page-submit.php');
const enqueueAt = source.indexOf('wp_enqueue_editor();');
const headerAt = source.indexOf('\nget_header();');
assert.notEqual(enqueueAt, -1, 'front submit must explicitly enqueue the editor runtime');
assert.notEqual(headerAt, -1, 'front submit must render the theme header');
assert.ok(enqueueAt < headerAt, 'editor assets must be queued before get_header()/wp_head');
});
test('front submit uploads use the effective WordPress server limit', () => {
const controller = read('src/Core/FrontSubmit.php');
const page = read('templates/pages/page-submit.php');
const limits = read('src/Attachment/UploadLimits.php');
assert.match(controller, /function upload_limit_bytes\(string \$kind\): int/);
assert.match(controller, /UploadLimits::limit_bytes\(\$config_kind\)/);
assert.match(controller, /\$config_kind = \$kind === 'resource' \? 'file' : \$kind;/);
assert.match(controller, /UPLOAD_ERR_INI_SIZE/);
assert.match(controller, /UPLOAD_ERR_FORM_SIZE/);
assert.match(controller, /UPLOAD_ERR_PARTIAL/);
assert.match(controller, /\['status' => 413\]/);
// The effective clamp (configured limit vs PHP upload cap) lives in the limiter.
assert.match(limits, /min\(\$mb \* MB_IN_BYTES, \(int\) wp_max_upload_size\(\)\)/);
assert.match(page, /FrontSubmit::upload_limit_bytes\('image'\)/);
assert.match(page, /FrontSubmit::upload_limit_bytes\('video'\)/);
assert.match(page, /FrontSubmit::upload_limit_bytes\('resource'\)/);
assert.doesNotMatch(page, /\$attachment_settings\s*=/);
});
test('front submit image batch selection follows the admin upload config', () => {
const page = read('templates/pages/page-submit.php');
const editor = read('assets/js/front-submit-editor.js');
assert.match(page, /UploadLimits::image_batch_limit\(\)/);
assert.match(page, /'imageUploadMultiple' => \$image_upload_multiple,/);
assert.match(editor, /function imageBatchLimit\(\)/);
assert.match(editor, /Number\(cfg\.imageUploadMultiple\)/);
assert.match(editor, /multiple: batch\.multiple, maxItems: batch\.maxItems/);
assert.match(editor, /type: 'image', multiple: imageBatch\.multiple, maxItems: imageBatch\.maxItems,/);
});
test('published post content reaches the front submit editor unchanged', () => {
const context = read('templates/parts/submit/context.php');
const editor = read('templates/parts/submit/editor-main.php');
assert.match(context, /\$current_content\s*=\s*\$edit_post\s*\?\s*\$edit_post->post_content\s*:\s*'';/);
assert.match(editor, /wp_editor\(\(string\) \$ctx\['current_content'\], 'post_content', \$editor_settings\);/);
});
test('front submit uses the WordPress-managed jQuery runtime', () => {
const source = read('src/Core/ScriptsPart1b.php');
assert.match(source, /wp_enqueue_script\('jquery'\)/);
assert.doesNotMatch(source, /wp_deregister_script\('jquery'\)/);
assert.doesNotMatch(source, /wp_register_script\('jquery'/);
assert.doesNotMatch(source, /jquery-migrate/);
assert.equal(fs.existsSync(path.join(root, 'assets/js/runtime/libs/jquery.min.js')), false);
assert.equal(fs.existsSync(path.join(root, 'assets/js/runtime/libs/jquery-ui.min.js')), false);
assert.doesNotMatch(read('inc/modules/user-parts/urls.php'), /aksrt_get_profile_url/);
const page = read('templates/pages/page-submit.php');
assert.match(page, /\['loader_js', 'aksrt-front-submit-editor', 'aksrt-front-submit-pay-download'\]/);
});
test('legacy runtime modules use jQuery explicitly without a dollar alias', () => {
const modules = [
'main',
'svg-icon',
'waterfall',
];
for (const module of modules) {
for (const suffix of ['.js', '.min.js']) {
const relative = `assets/js/runtime/${module}${suffix}`;
const absolute = path.join(root, relative);
if (!fs.existsSync(absolute) || fs.statSync(absolute).size === 0) continue;
const source = fs.readFileSync(absolute, 'utf8');
assert.doesNotMatch(source, /(^|[^\w$])\$(?:\(|\.)/, `${relative} must use jQuery explicitly`);
assert.doesNotMatch(source, /(?:window\.)?\$\s*=/, `${relative} must not create a dollar alias`);
}
}
});
test('theme runtime never changes native listener passivity for third-party editors', () => {
const scripts = read('src/Core/Scripts.php');
const scriptsPart2 = read('src/Core/ScriptsPart2.php');
const buildTool = read('tools/build-public-assets.mjs');
const publicRuntime = read('assets/js/runtime/main.min.js');
assert.doesNotMatch(scripts, /enqueue_passive_polyfill|aksrt-passive-events/);
assert.doesNotMatch(scriptsPart2, /enqueue_passive_polyfill|aksrt-passive-events/);
assert.doesNotMatch(buildTool, /passive-events\.js/);
assert.doesNotMatch(publicRuntime, /EventTarget\.prototype\.addEventListener|__passiveSupported/);
assert.equal(fs.existsSync(path.join(root, 'assets/js/passive-events.js')), false);
});
test('public theme does not inject a global focus-visible ring', () => {
const globalFocusRule = /(^|})\s*:focus-visible\s*\{/m;
const reset = read('assets/css/core/reset.css');
const publicCss = read('assets/css/build/public.css');
const publicMinCss = read('assets/css/build/public.min.css');
assert.doesNotMatch(reset, globalFocusRule);
assert.doesNotMatch(publicCss, globalFocusRule);
assert.doesNotMatch(publicMinCss, globalFocusRule);
});
test('non-draft front-submit updates require one server captcha admission', () => {
const controller = read('src/Core/FrontSubmit.php');
// Edits ride the same submit endpoint (posts_id + posts_save), which is the
// only non-draft entry point and always verifies captcha before persisting.
const start = controller.indexOf('public function ajax_submit_post(): void');
const end = controller.indexOf('\n public function ajax_save_draft', start);
const body = controller.slice(start, end);
assert.match(body, /\$this->verify_nonce\(\);\s*\$this->verify_captcha\(\);\s*\$data = wp_unslash\(\$_POST\);\s*\$result = \$this->persist\(is_array\(\$data\) \? \$data : \[\], 'posts_save'\);/);
assert.match(controller, /get_error_code\(\)/);
assert.match(controller, /get_error_message\(\)/);
});
test('front submit TinyMCE status bar shows one live character counter', () => {
const editor = read('assets/js/front-submit-editor.js');
const submitCss = read('assets/css/frontend/pages/submit.css');
const legacyPageEdit = read('assets/js/page-edit.js');
assert.match(editor, /querySelector\('\.mce-statusbar \.mce-container-body'\)/);
assert.match(editor, /data-submit-word-count/);
assert.match(editor, /textContent\s*=\s*'字数:'\s*\+\s*count/);
assert.match(editor, /editor\.on\('init SetContent input keyup change Undo Redo'/);
assert.match(editor, /replace\(\/\\s\+\/g, ''\)\.length/);
assert.match(submitCss, /\.newposts-editor \.aksrt-submit-word-count\s*{[^}]*float:\s*right;/);
assert.doesNotMatch(legacyPageEdit, /initWordCount|updateWordCount|aksrt-page-edit__word-count|字数:/);
});