'use strict'; const assert = require('node:assert/strict'); const { execFileSync } = require('node:child_process'); 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'); const SYSTEM_FIELDS = [ 'hide_admin_bar', 'remove_emoji', 'remove_open_sans', 'remove_more_wp_head', 'remove_dashboard_widgets', 'display_wp_update', 'split_upload_s', 'split_minimum_size', 'newfilename', 'newfilename_type', 'disabled_large_thumbnail', 'admin_user_del_fields', 'admin_frontend_set', 'close_gutenberg', 'disabled_autoembed', 'mce_float_toolbar', 'time_ago_s', 'time_format', ]; test('system tools schema registers the 系统工具 page under 扩展&增强', () => { const schema = read('src/Admin/Schema/SchemaExtensionsSystem.php'); const registry = read('src/Admin/Schema/SchemaExtensions.php'); const clientMirror = read('assets/admin/src/schema/sections/extensions/system.js'); const clientIndex = read('assets/admin/src/schema/sections/extensions/index.js'); const nav = read('assets/admin/src/constants/sections/extensions.js'); const sectionDefaults = read('assets/admin/src/constants/sections/index.js'); assert.match(registry, /'system'\s+=> SchemaExtensionsSystem::build\(\)/); assert.match(schema, /'key'\s+=> 'system'/); assert.match(schema, /'title'\s+=> '系统工具'/); for (const key of SYSTEM_FIELDS) { assert.match(schema, new RegExp(`'${key}'`), `missing schema field ${key}`); assert.match(clientMirror, new RegExp(`${key}`), `missing client mirror field ${key}`); } // Conditional fields mirror the reference theme's dependency rules. assert.match(schema, /'newfilename_type'[^[]*'dependsOn'\s+=> \['key' => 'newfilename', 'value' => true\]/s); assert.match(schema, /'split_minimum_size'[^[]*'dependsOn'\s+=> \['key' => 'split_upload_s', 'value' => true\]/s); assert.match(schema, /'time_format'[^[]*'dependsOn'\s+=> \['key' => 'time_ago_s', 'operator' => 'falsy'\]/s); assert.match(clientMirror, /split_minimum_size[\s\S]*dependsOn:\s*\{\s*key:\s*'split_upload_s',\s*value:\s*true\s*\}/); assert.match(clientMirror, /time_format[\s\S]*dependsOn:\s*\{\s*key:\s*'time_ago_s',\s*operator:\s*'falsy'\s*\}/); assert.match(clientIndex, /import \{ systemSchema \} from '\.\/system\.js'/); assert.match(clientIndex, /system: systemSchema/); assert.match(nav, /key: 'extensions-system'/); assert.match(nav, /route: '\/extensions\/system'/); assert.match(nav, /section: 'system'/); // 系统工具 is the first child of 扩展&增强 (reference theme order) and the default landing page. assert.ok(nav.indexOf('extensions-system') < nav.indexOf('extensions-ip'), '系统工具 should precede IP归属地'); assert.match(sectionDefaults, /extensions:\s*'extensions-system'/); }); test('system tools settings are consumed at runtime and initialized on boot', () => { const runtime = read('src/Settings/ExtensionRuntime.php'); const bootstrap = read('functions.php'); assert.match(runtime, /public static function get_system_tools_settings\(\): array/); assert.match(runtime, /'newfilename_type'\s+=> \$rename_type/); assert.match(runtime, /'split_minimum_size'\s+=> max\(4, min\(100, \(int\) \(\$settings\['split_minimum_size'\] \?\? 20\)\)\)/); assert.match(bootstrap, /\\AKSRT\\Extensions\\SystemTools::get_instance\(\)->init\(\)/); assert.match(bootstrap, /\\AKSRT\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/); }); test('system tools switches wire the reference theme hook suite', () => { const tools = read('src/Extensions/SystemTools.php'); assert.match(tools, /!empty\(\$settings\['hide_admin_bar'\]\) \|\| !is_super_admin\(\)/); assert.match(tools, /remove_action\('wp_head', 'print_emoji_detection_script', 7\)/); assert.match(tools, /remove_filter\('wp_mail', 'wp_staticize_emoji_for_email'\)/); assert.match(tools, /add_action\('init', \[\$this, 'remove_open_sans'\]\)/); assert.match(tools, /wp_deregister_style\('open-sans'\)/); assert.match(tools, /add_action\('after_setup_theme', \[\$this, 'remove_more_wp_head'\]\)/); assert.match(tools, /remove_action\('wp_head', 'wp_generator'\)/); assert.match(tools, /add_action\('do_meta_boxes', \[\$this, 'remove_dashboard_widgets'\], 10, 3\)/); assert.match(tools, /unset\(\$wp_meta_boxes\['dashboard'\]\['normal'\]\['high'\]\['dashboard_php_nag'\]\)/); assert.match(tools, /add_filter\('site_status_tests', \[\$this, 'filter_site_status_tests'\]\)/); assert.match(tools, /add_filter\('pre_http_request', \[\$this, 'block_site_health_requests'\], 10, 3\)/); assert.match(tools, /'api\.wordpress\.org\/core\/serve-happy\/'/); assert.match(tools, /remove_action\('admin_init', '_maybe_update_core'\)/); assert.match(tools, /add_filter\('automatic_updater_disabled', '__return_true'\)/); assert.match(tools, /remove_action\('wp_version_check', 'wp_version_check'\)/); assert.match(tools, /add_action\('admin_menu', \[\$this, 'hide_update_notices'\]\)/); assert.match(tools, /remove_action\('admin_notices', 'update_nag', 3\)/); assert.match(tools, /remove_image_size\('1536x1536'\)/); assert.match(tools, /add_filter\('intermediate_image_sizes', static function \(\$sizes\)/); assert.match(tools, /add_filter\('use_block_editor_for_post', '__return_false'\)/); assert.match(tools, /remove_filter\('the_content', \[\$GLOBALS\['wp_embed'\], 'autoembed'\], 8\)/); assert.match(tools, /add_action\('show_user_profile', \[\$this, 'hide_user_profile_fields'\]\)/); assert.match(tools, /add_action\('edit_user_profile', \[\$this, 'hide_user_profile_fields'\]\)/); }); test('upload rename settings are canonical to 系统工具 and consumed by the upload pipeline', () => { const permissionsSchema = read('src/Admin/Schema/SchemaPermissions.php'); const permissionsMirror = read('assets/admin/src/schema/sections/permissions/index.js'); const systemSchema = read('src/Admin/Schema/SchemaExtensionsSystem.php'); const uploadLimits = read('src/Attachment/UploadLimits.php'); assert.doesNotMatch(permissionsSchema, /newfilename/); assert.doesNotMatch(permissionsMirror, /newfilename/); assert.match(systemSchema, /'newfilename'/); assert.match(uploadLimits, /add_filter\('wp_handle_upload_prefilter', \[\$this, 'rename_upload_file'\]/); assert.match(uploadLimits, /\$settings\['newfilename_type'\] \?\? 'time'\) === 'random'/); }); test('split upload ships a full chunk protocol: routes, merge pipeline, and client uploader', () => { const chunk = read('src/Core/FrontSubmitChunk.php'); const frontSubmit = read('src/Core/FrontSubmit.php'); const editorJs = read('assets/js/front-submit-editor.js'); const submitPage = read('templates/pages/page-submit.php'); const schema = read('src/Admin/Schema/SchemaExtensionsSystem.php'); // The submit class consumes the chunk trait and registers its routes. assert.match(frontSubmit, /use FrontSubmitChunk;/); assert.match(frontSubmit, /\$this->register_chunk_routes\(\)/); assert.match(frontSubmit, /public function persist_uploaded_file\(string \$kind, array \$file, bool \$sideload = false\)/); // Backend mirrors the reference theme's user_split_upload protocol. assert.match(chunk, /\/front-submit\/upload-chunk\/\(\?Pimage\|video\|resource\)/); assert.match(chunk, /\/front-submit\/upload-merge\(\?Pimage\|video\|resource\)|\/front-submit\/upload-merge\/\(\?Pimage\|video\|resource\)/); assert.match(chunk, /\/front-submit\/upload-status\/\(\?Pimage\|video\|resource\)/); assert.match(chunk, /permission_callback' => \[\$this, 'can_upload_attachment'\]/); assert.match(chunk, /const CHUNK_SIZE_BYTES = 2 \* 1048576;/); assert.match(chunk, /md5\(get_current_blog_id\(\) \. '\|' \. get_current_user_id\(\) \. '\|' \. \$name\)/); assert.match(chunk, /\$this->persist_uploaded_file\(\$params\['kind'\], \[/); assert.match(chunk, /apply_filters\('aksrt_temp_dir', WP_CONTENT_DIR \. '\/aksrt-temp'\)/); assert.match(chunk, /\$this->chunk_temp_cleanup\(\$temp_dir\)/); // Whole-file upload and chunk merge share one persistence pipeline; merged // files are not HTTP uploads, so the pipeline must use the sideload action. assert.match(frontSubmit, /return \$this->persist_uploaded_file\(\$kind, \$file, false\);/); assert.match(frontSubmit, /'action' => 'wp_handle_sideload'/); assert.match(frontSubmit, /media_handle_upload\('aksrt_front_submit_file', 0, \[\], \$overrides\)/); // Client uploader picks the chunked path past the threshold and resumes from status. assert.match(editorJs, /function uploadInChunks\(file, type\)/); assert.match(editorJs, /splitUploadConfig\(\)/); assert.match(editorJs, /file\.size > split\.minSize \* 1048576/); assert.match(editorJs, /chunkUrl\('upload-status'\)/); assert.match(editorJs, /chunkUrl\('upload-chunk'\)/); assert.match(editorJs, /chunkUrl\('upload-merge'\)/); assert.match(editorJs, /'\/front-submit\/' \+ action \+ '\/' \+ uploadKey/); // Config flows from the settings page into aksrtSubmitData. assert.match(submitPage, /'splitUpload' => \[/); assert.match(submitPage, /'enabled' => !empty\(\$system_tools\['split_upload_s'\]\)/); assert.match(submitPage, /'minSize' => \(int\) \(\$system_tools\['split_minimum_size'\] \?\? 20\)/); assert.match(schema, /'split_upload_s'/); assert.match(schema, /'split_minimum_size'/); // The legacy dead config keys stay out of the shared script payload. assert.doesNotMatch(read('src/Core/ScriptsPart1c.php'), /is_split_upload|split_minimum_size/); }); test('frontend page edit ships gate, float button, modal, and whitelisted save', () => { const pageAdminSet = read('src/Frontend/PageAdminSet.php'); const pageAdminJs = read('assets/js/page-admin-set.js'); const floatButtons = read('inc/helpers/aksrt-float-buttons.php'); const bootstrap = read('functions.php'); assert.match(bootstrap, /\\AKSRT\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/); assert.match(pageAdminSet, /public static function enabled\(\): bool/); assert.match(pageAdminSet, /empty\(\$settings\['admin_frontend_set'\]/); assert.match(pageAdminSet, /current_user_can\('manage_options'\)/); assert.match(pageAdminSet, /is_page_template\('templates\/pages\/page-submit\.php'\)/); assert.match(pageAdminSet, /is_page_template\('templates\/pages\/page-posts-navs\.php'\)/); // Float bar gains the same extension point the reference theme hooks into. assert.match(floatButtons, /apply_filters\('aksrt_float_right_buttons', \$btn, \$is_mobile\)/); assert.match(pageAdminSet, /add_filter\('aksrt_float_right_buttons', \[\$this, 'float_button'\], 10, 2\)/); assert.match(pageAdminSet, /data-aksrt-modal="remote"/); assert.match(pageAdminSet, /check_ajax_referer\('aksrt_frontend_set', '_wpnonce'\)/); // Modal + save endpoints; save is whitelisted and preserves other meta keys. assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_modal', \[\$this, 'ajax_modal'\]\)/); assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_save', \[\$this, 'ajax_save'\]\)/); assert.match(pageAdminSet, /\\AKSRT\\Core\\PostMetaBox::META_KEY/); assert.match(pageAdminSet, /\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/); assert.match(pageAdminSet, /update_post_meta\(\$post_id, '_aksrt_like_count', max\(0, \(int\) \$_POST\['like'\]\)\)/); assert.match(pageAdminSet, /wp_update_term\(\$term_id, \(string\) \$term->taxonomy, \$data\)/); assert.match(pageAdminSet, /set_post_format\(\$post_id, \$format === 'standard' \? '' : \$format\)/); // Client submits through the shared modal runtime contract; the ajax action // comes from the form's hidden field that the modal endpoint embeds. assert.match(pageAdminJs, /form\.aksrt-admin-set-form/); assert.match(pageAdminJs, /input\[name="action"\]/); assert.match(pageAdminJs, /window\.location\.reload/); assert.match(pageAdminSet, /'html' => \$html/); }); test('float toolbar config reaches the tinymce plugin with reference-context buttons', () => { const editorJs = read('assets/js/front-submit-editor.js'); const submitPage = read('templates/pages/page-submit.php'); const editorTemplate = read('templates/parts/submit/editor-main.php'); const schema = read('src/Admin/Schema/SchemaExtensionsSystem.php'); assert.match(submitPage, /\$float_toolbar = wp_is_mobile\(\) \? in_array\('m_s', \$float_scenes, true\) : in_array\('pc_s', \$float_scenes, true\)/); assert.match(submitPage, /'floatToolbar'\s+=> \$float_toolbar/); assert.match(editorJs, /if \(editorCfg\.floatToolbar\) \{\s*\n\s*registerFloatToolbar\(editor\);/); assert.match(editorJs, /editor\.on\('wptoolbar', function \(event\)/); assert.match(editorJs, /editor\.wp\._createToolbar\(bars\)/); assert.match(editorJs, /editor\.addButton\('aksrt_h2'/); assert.match(editorJs, /editor\.addButton\('aksrt_h3'/); assert.match(editorJs, /editor\.addButton\('aksrt_remove'/); assert.match(editorJs, /function floatToolbarButtons\(editor, event\)/); assert.match(schema, /'mce_float_toolbar', 'label' => '经典编辑器增强浮动工具栏', 'type' => 'checkbox'/); // The editor template still wires the plugin into wp_editor. assert.match(editorTemplate, /aksrt_editor_tools/); }); test('countdown display switch gates aksrt_time_ago with the custom format fallback', () => { const helper = read('inc/helpers/template-tags/search-meta.php'); const coreHooks = read('inc/core-hooks.php'); assert.match(helper, /empty\(\$system\['time_ago_s'\]\) && \(\$system\['time_format'\] \?\? ''\) !== ''/); assert.match(helper, /return date\(\$system\['time_format'\], \$time\)/); // Admin bar gating moved out of the unconditional core hook into SystemTools. assert.doesNotMatch(coreHooks, /add_filter\('show_admin_bar', '__return_false'\)/); }); test('system tools runtime contract harness passes', () => { const harness = path.join(root, 'tests', 'widget-system', 'system-tools-harness.php'); const output = execFileSync('php', [harness], { encoding: 'utf8' }); assert.match(output, /System tools harness passed/); }); test('split upload runtime contract harness passes', () => { const harness = path.join(root, 'tests', 'widget-system', 'split-upload-harness.php'); const output = execFileSync('php', [harness], { encoding: 'utf8' }); assert.match(output, /Split upload harness passed/); });