refactor: rename the theme brand and every identifier from AKSRT to Jio

Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-01 20:05:04 +08:00
1 parent 5b16bc6e15
commit 35bb96e82f
997 files changed
+28216 -28188

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ const submitContext = read('templates/parts/submit/context.php');
const submitPage = read('templates/pages/page-submit.php');
const removed = [
'aksrt_ai_settings',
'jio_ai_settings',
'AI 连接配置',
'AI连接配置',
'ExtensionAI',
+9 -9
View File
@@ -45,7 +45,7 @@ namespace {
function wp_nonce_field($action, $name) { echo '<input type="hidden" name="' . $name . '" value="nonce" />'; }
function wp_unslash($s) { return $s; }
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
function wp_verify_nonce($nonce, $action) { return $nonce === 'valid-nonce' && $action === 'aksrt_push_meta_save'; }
function wp_verify_nonce($nonce, $action) { return $nonce === 'valid-nonce' && $action === 'jio_push_meta_save'; }
function is_wp_error($t) { return $t instanceof WP_Error; }
function home_url($path = '/') { return 'https://example.test' . $path; }
function current_time($type = 'mysql') { return '2026-10-13 12:00:00'; }
@@ -56,7 +56,7 @@ namespace {
}
function wp_is_post_revision($id) { return false; }
function wp_is_post_autosave($id) { return false; }
function aksrt_searchable_post_types(): array { return ['post']; }
function jio_searchable_post_types(): array { return ['post']; }
function get_object_taxonomies($types, $output = 'names') { return ['post_tag', 'category', 'post_format']; }
// Outgoing HTTP: every call is captured so channel/URL/body can be asserted.
@@ -79,7 +79,7 @@ namespace {
function delete_term_meta($id, $key) { unset($GLOBALS['push_term_meta'][$id][$key]); return true; }
}
namespace AKSRT\Settings {
namespace Jio\Settings {
class Settings {
public static array $config = [];
public static function get_search_push_settings(): array { return self::$config; }
@@ -90,8 +90,8 @@ namespace {
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPush.php';
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPushMetaBox.php';
use AKSRT\Extensions\SearchPush;
use AKSRT\Extensions\SearchPushMetaBox;
use Jio\Extensions\SearchPush;
use Jio\Extensions\SearchPushMetaBox;
$failures = [];
$check = static function (string $label, bool $ok) use (&$failures): void {
@@ -101,7 +101,7 @@ namespace {
};
$reset = static function (array $config): void {
\AKSRT\Settings\Settings::$config = $config;
\Jio\Settings\Settings::$config = $config;
$GLOBALS['push_requests'] = [];
$GLOBALS['push_post_meta'] = [];
$GLOBALS['push_term_meta'] = [];
@@ -236,8 +236,8 @@ namespace {
$check('面板含普通收录成功', str_contains($html, '普通收录:成功'));
$check('面板含快速收录失败', str_contains($html, '快速收录:失败'));
$check('面板含更新时间', str_contains($html, '2026-10-13 09:00:00'));
$check('面板含重新提交勾选', str_contains($html, 'name="aksrt_push_resubmit"'));
$check('面板含 nonce 字段', str_contains($html, 'name="aksrt_push_meta_nonce"'));
$check('面板含重新提交勾选', str_contains($html, 'name="jio_push_resubmit"'));
$check('面板含 nonce 字段', str_contains($html, 'name="jio_push_meta_nonce"'));
$check('面板转义接口 JSON', str_contains($html, '{&quot;success&quot;:1}'));
ob_start();
@@ -251,7 +251,7 @@ namespace {
$box->render_term_panel($term);
$termHtml = (string) ob_get_clean();
$check('术语编辑表单为表单行', str_contains($termHtml, '<tr class="form-field">'));
$check('术语编辑表单含重新提交', str_contains($termHtml, 'name="aksrt_push_resubmit"'));
$check('术语编辑表单含重新提交', str_contains($termHtml, 'name="jio_push_resubmit"'));
if ($failures) {
echo "Baidu push harness FAILED\n";
@@ -27,7 +27,7 @@ const displayGroups = fs.readFileSync(path.join(root, 'src/Settings/DisplayPageG
const metaBox = fs.readFileSync(path.join(root, 'src/Extensions/SearchPushMetaBox.php'), 'utf8');
const functions = fs.readFileSync(path.join(root, 'functions.php'), 'utf8');
// 参考主题(zibll)百度推送字段全集,AKSRT 单一 canonical 配置面(扩展&增强 → 百度推送)。
// 参考主题(zibll)百度推送字段全集,Jio 单一 canonical 配置面(扩展&增强 → 百度推送)。
const REFERENCE_KEYS = [
'baidu_push_enabled',
'baidu_push_token',
@@ -136,7 +136,7 @@ test('前台自动推送 JS 与参考主题一致', () => {
test('发布/更新触发沿用 transition_post_status 且仅限可检索内容类型', () => {
assert.match(runtime, /transition_post_status/);
assert.match(runtime, /aksrt_searchable_post_types/);
assert.match(runtime, /jio_searchable_post_types/);
assert.match(runtime, /wp_is_post_revision/);
assert.match(runtime, /wp_is_post_autosave/);
});
@@ -156,12 +156,12 @@ test('分类法推送接入 saved_term 且排除文章形式', () => {
assert.match(runtime, /add_action\('saved_term'/);
assert.match(runtime, /public static function pushable_taxonomies/);
assert.match(runtime, /array_diff\(\(array\) \$taxonomies, \['post_format', 'nav_menu'\]\)/);
assert.match(runtime, /aksrt_baidu_push_taxonomies/);
assert.match(runtime, /jio_baidu_push_taxonomies/);
assert.match(runtime, /get_term_link\(\$term_id, \$taxonomy\)/);
});
test('推送结果写入 meta 并按记录去重', () => {
assert.match(runtime, /META_KEY\s*=\s*'_aksrt_baidu_push_back'/);
assert.match(runtime, /META_KEY\s*=\s*'_jio_baidu_push_back'/);
assert.match(runtime, /private function record_is_complete/);
assert.match(runtime, /private function build_record/);
assert.match(runtime, /'normal_push'/);
@@ -172,14 +172,14 @@ test('推送结果写入 meta 并按记录去重', () => {
});
test('「重新提交」勾选需通过 metabox nonce 校验', () => {
assert.match(runtime, /NONCE_ACTION\s*=\s*'aksrt_push_meta_save'/);
assert.match(runtime, /NONCE_FIELD\s*=\s*'aksrt_push_meta_nonce'/);
assert.match(runtime, /RESUBMIT_FIELD\s*=\s*'aksrt_push_resubmit'/);
assert.match(runtime, /NONCE_ACTION\s*=\s*'jio_push_meta_save'/);
assert.match(runtime, /NONCE_FIELD\s*=\s*'jio_push_meta_nonce'/);
assert.match(runtime, /RESUBMIT_FIELD\s*=\s*'jio_push_resubmit'/);
assert.match(runtime, /wp_verify_nonce\(\$nonce, self::NONCE_ACTION\)/);
});
test('编辑页面板与参考主题「百度资源提交」Metabox 对齐', () => {
assert.match(metaBox, /add_meta_box\(\s*'aksrt_baidu_push',\s*__\('百度资源提交', 'aksrt'\)/);
assert.match(metaBox, /add_meta_box\(\s*'jio_baidu_push',\s*__\('百度资源提交', 'jio'\)/);
assert.match(metaBox, /'side',\s*'high'/);
assert.match(metaBox, /\{\$taxonomy\}_edit_form_fields/);
assert.match(metaBox, /\{\$taxonomy\}_add_form_fields/);
+15 -15
View File
@@ -84,10 +84,10 @@ function extractCalls(source, marker) {
/** 收集一行渲染调用携带的依赖规则与显示标签。 */
function rowsWithDeps(source) {
const markers = [
{ marker: '->field_row(', label: (t) => (t.match(/__\(\s*'([^']*)'\s*,\s*'aksrt'\s*\)/) || [])[1] },
{ marker: '->field_row(', label: (t) => (t.match(/__\(\s*'([^']*)'\s*,\s*'jio'\s*\)/) || [])[1] },
{
marker: '->submessage_row(',
label: (t) => (t.match(/<b>'\s*\.\s*esc_html__\(\s*'([^']*)'\s*,\s*'aksrt'\s*\)/) || [])[1],
label: (t) => (t.match(/<b>'\s*\.\s*esc_html__\(\s*'([^']*)'\s*,\s*'jio'\s*\)/) || [])[1],
},
];
const calls = [];
@@ -147,7 +147,7 @@ function createForm() {
const document = {
readyState: 'loading',
querySelectorAll(sel) {
if (sel === '.aksrt-cardpass-form [name]') return inputs;
if (sel === '.jio-cardpass-form [name]') return inputs;
if (sel === '[data-dep]') return rows.map((row) => row.el);
return [];
},
@@ -220,10 +220,10 @@ test('两种行渲染都接入服务端预隐藏,避免首帧铺开', () => {
// wp-admin/js/common.js 会把页面上的 notice 搬到页头,见下一条用例),
// 它仍能被求值并保持隐藏。
assert.match(page, /<td colspan="2"><div'\s*\.\s*\$dep_attr/, '提示块必须自带 data-dep');
assert.match(page, /class="aksrt-submsg"'\s*\.\s*\$hidden/, '提示块必须自带初始隐藏状态');
assert.match(page, /class="jio-submsg"'\s*\.\s*\$hidden/, '提示块必须自带初始隐藏状态');
// 求值范围不限域:提示块被移出表格/表单子树后仍必须被求值。
assert.match(script, /querySelectorAll\('\[data-dep\]'\)/, '求值必须全文档查找 data-dep');
assert.doesNotMatch(script, /querySelectorAll\('\.aksrt-cardpass-form \[data-dep\]'\)/, '求值不得限域在表单内');
assert.doesNotMatch(script, /querySelectorAll\('\.jio-cardpass-form \[data-dep\]'\)/, '求值不得限域在表单内');
});
test('导入提示不得使用 WP 的 .notice 类(否则被 common.js 搬到页面顶部)', () => {
@@ -234,19 +234,19 @@ test('导入提示不得使用 WP 的 .notice 类(否则被 common.js 搬到
// 且搬迁发生在 evalDeps 之后,行内显隐逻辑无法纠正(历史故障:提示一直显示在页面最上方)。
assert.match(
page,
/private function submessage_row\([\s\S]*?class="aksrt-submsg"/,
'提示块只应带自有类名 aksrt-submsg'
/private function submessage_row\([\s\S]*?class="jio-submsg"/,
'提示块只应带自有类名 jio-submsg'
);
assert.doesNotMatch(
page,
/class="aksrt-submsg[^"]*\bnotice\b/,
/class="jio-submsg[^"]*\bnotice\b/,
'提示块不得带 notice 类:会被 common.js 搬到页面顶部'
);
// 外观改由自有类名承担,删掉样式规则会让提示失去底色与左侧色条。
assert.match(
page,
/\.aksrt-submsg \{[^}]*background:[^}]*border-left:/,
'.aksrt-submsg 必须自带 background 与 border-left 外观'
/\.jio-submsg \{[^}]*background:[^}]*border-left:/,
'.jio-submsg 必须自带 background 与 border-left 外观'
);
// 允许(且要求)真正需要置顶的提示保留 notice:确认本用例没有误伤其它通知输出。
assert.match(page, /class="notice notice-error"/, '页面级错误通知仍应使用 notice');
@@ -266,17 +266,17 @@ test('导入提示的条目列表由 <ul> 承载,圆点不越出文字边界',
checked++;
assert.match(
call.text,
/<ul class="aksrt-submsg-list">/,
'提示内的 <li> 必须包在 <ul class="aksrt-submsg-list"> 中'
/<ul class="jio-submsg-list">/,
'提示内的 <li> 必须包在 <ul class="jio-submsg-list"> 中'
);
assert.equal([...call.text.matchAll(/<ul class="aksrt-submsg-list">/g)].length, 1, '每条提示只应有一个条目列表');
assert.equal([...call.text.matchAll(/<ul class="jio-submsg-list">/g)].length, 1, '每条提示只应有一个条目列表');
assert.equal(opens, [...call.text.matchAll(/<\/li>/g)].length, '<li> 标签必须成对');
assert.equal([...call.text.matchAll(/<\/ul>/g)].length, 1, '</ul> 必须成对');
}
assert.equal(checked, 4, '四条导入提示都应带条目列表');
assert.match(page, /\.aksrt-submsg-list \{[^}]*padding-left:/, '.aksrt-submsg-list 必须给出 padding-left,圆点才有落点');
assert.match(page, /\.aksrt-submsg-list \{[^}]*list-style:\s*disc/, '.aksrt-submsg-list 应显式声明圆点样式,不依赖 UA 默认值');
assert.match(page, /\.jio-submsg-list \{[^}]*padding-left:/, '.jio-submsg-list 必须给出 padding-left,圆点才有落点');
assert.match(page, /\.jio-submsg-list \{[^}]*list-style:\s*disc/, '.jio-submsg-list 应显式声明圆点样式,不依赖 UA 默认值');
});
test('行内脚本不再保留解析期的空操作求值(死代码)', () => {
@@ -156,7 +156,7 @@ test('整个后台页源码内没有任何输入携带原生约束属性', () =>
test('表单不用 novalidate 掩盖问题,且提交按钮仍是原生 submit', () => {
assert.doesNotMatch(addForm, /novalidate/i, '不得通过关闭校验来绕过,应去掉约束本身');
assert.match(addForm, /submit_button\(\s*__\('确认提交',\s*'aksrt'\)/);
assert.match(addForm, /submit_button\(\s*__\('确认提交',\s*'jio'\)/);
assert.match(addForm, /wp_nonce_field\(self::NONCE\)/);
});
@@ -48,8 +48,8 @@ test('标题右侧按钮只在页面头部输出一次', () => {
// 列表页的两个入口按钮各自只有一处定义。
assert.equal(count(page, /add_query_arg\('tab', 'add'/g), 1);
assert.equal(count(page, /add_query_arg\('tab', 'export'/g), 1);
assert.match(header, /esc_html__\('添加卡密', 'aksrt'\)/);
assert.match(header, /esc_html__\('导出卡密', 'aksrt'\)/);
assert.match(header, /esc_html__\('添加卡密', 'jio'\)/);
assert.match(header, /esc_html__\('导出卡密', 'jio'\)/);
});
test('搜索提示仍由页面头部输出且列表不再自建头部', () => {
+1 -1
View File
@@ -77,7 +77,7 @@ test('ice_perpage 仍作为 URL 参数被尊重,默认每页 20', () => {
// 明确约定:不在列表工具栏增加「每页显示」下拉/按钮。
assert.doesNotMatch(page, /per_page_selector/);
assert.doesNotMatch(page, /PER_PAGE_CHOICES/);
assert.doesNotMatch(page, /aksrt-perpage-form/);
assert.doesNotMatch(page, /jio-perpage-form/);
assert.doesNotMatch(page, /name="ice_perpage"/);
assert.doesNotMatch(page, /order-header-right/);
@@ -13,7 +13,7 @@
* 3) 插入表情(comment_smilie)、评论地理位置(comment_city_s/comment_city_type,
* 由扩展&增强/IP归属地实现)不在本页;
* 4) 快捷回复按评论区硬切换契约保持移除(comment_quick_* 不存在);
* 5) 旧 AKSRT 私有字段(comment_order / comment_per_page / comment_pagination_type /
* 5) 旧 Jio 私有字段(comment_order / comment_per_page / comment_pagination_type /
* comment_nested_depth / comment_avatar_size / comment_close_comments /
* comment_edit_window / comment_hot_threshold / comment_empty_text /
* post_close_comments)已彻底移除,无双路兼容;
@@ -29,7 +29,7 @@ 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/comment.js'), 'utf8');
const runtime = fs.readFileSync(path.join(root, 'src/Settings/PostDisplay.php'), 'utf8');
const commentsHelper = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-comments.php'), 'utf8');
const commentsHelper = fs.readFileSync(path.join(root, 'inc/helpers/jio-comments.php'), 'utf8');
const geoHelper = fs.readFileSync(path.join(root, 'inc/helpers/comments/geo.php'), 'utf8');
const actionsHelper = fs.readFileSync(path.join(root, 'inc/helpers/comments/actions.php'), 'utf8');
@@ -62,7 +62,7 @@ const EXCLUDED_KEYS = [
'comment_quick_often',
];
// 旧 AKSRT 私有字段(含已并入 WP 讨论设置与死字段 post_close_comments)。
// 旧 Jio 私有字段(含已并入 WP 讨论设置与死字段 post_close_comments)。
const LEGACY_KEYS = [
'comment_order',
'comment_per_page',
@@ -204,7 +204,7 @@ test('旧字段在 schema 与运行时中均已移除(无双路)', () => {
assert.ok(!php.includes(legacy), `PHP schema 残留旧字段: ${legacy}`);
// 运行时按主题设置读取的形态 $settings['<legacy>'] 必须消失
// (comment_order 允许以 WP 讨论选项 get_option('comment_order') 形态存在)。
for (const source of [['PostDisplay', runtime], ['aksrt-comments.php', commentsHelper], ['comments/geo.php', geoHelper], ['comments/actions.php', actionsHelper]]) {
for (const source of [['PostDisplay', runtime], ['jio-comments.php', commentsHelper], ['comments/geo.php', geoHelper], ['comments/actions.php', actionsHelper]]) {
assert.ok(!source[1].includes(`$settings['${legacy}']`), `${source[0]} 残留旧字段读取: ${legacy}`);
}
}
@@ -217,8 +217,8 @@ test('旧字段在 schema 与运行时中均已移除(无双路)', () => {
// 排序回退到 WP 讨论设置(参考主题行为)。
assert.match(runtime, /get_option\('comment_order', 'desc'\)/);
// 评论区位置显示交由扩展 IP 归属地控制。
assert.match(geoHelper, /aksrt_ip_geo_display_enabled\('comment'\)/);
assert.match(geoHelper, /aksrt_ip_geo_format_key/);
assert.match(geoHelper, /jio_ip_geo_display_enabled\('comment'\)/);
assert.match(geoHelper, /jio_ip_geo_format_key/);
// 编辑权限使用 user_edit 总开关。
assert.match(actionsHelper, /\$display\['user_edit'\]/);
});
@@ -8,7 +8,7 @@
* imagelightbox / imagelightbox_type / imagelightbox_thumbs_s /
* imagelightbox_play_s / imagelightbox_zoom_s / imagelightbox_full_s /
* imagelightbox_down_s / post_like_s / post_default_mate(views/like);
* 3) 旧 AKSRT 私有/异名字段(feature_print / feature_pdf_export /
* 3) 旧 Jio 私有/异名字段(feature_print / feature_pdf_export /
* post_lightbox* / post_like_enabled / post_default_*_min/max)已彻底移除,
* 文章页面(page)不再承载这些字段,无双路兼容;
* 4) 运行时(PostDisplay::get_post_page_settings)只读取新键,灯箱复选框
@@ -27,10 +27,10 @@ const pageJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections
const runtime = fs.readFileSync(path.join(root, 'src/Settings/PostDisplay.php'), 'utf8');
const scripts1c = fs.readFileSync(path.join(root, 'src/Core/ScriptsPart1c.php'), 'utf8');
const scripts1b = fs.readFileSync(path.join(root, 'src/Core/ScriptsPart1b.php'), 'utf8');
const frontendTags = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-frontend-tags.php'), 'utf8');
const frontendTags = fs.readFileSync(path.join(root, 'inc/helpers/jio-frontend-tags.php'), 'utf8');
const coreHooks = fs.readFileSync(path.join(root, 'inc/core-hooks.php'), 'utf8');
const singleRender = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-single-render.php'), 'utf8');
const mobileHeader = fs.readFileSync(path.join(root, 'inc/helpers/header/aksrt-header-mobile.php'), 'utf8');
const singleRender = fs.readFileSync(path.join(root, 'inc/helpers/jio-single-render.php'), 'utf8');
const mobileHeader = fs.readFileSync(path.join(root, 'inc/helpers/header/jio-header-mobile.php'), 'utf8');
const REFERENCE_KEYS = [
'article_nav',
@@ -46,7 +46,7 @@ const REFERENCE_KEYS = [
'post_default_mate',
];
// 旧 AKSRT 私有 / 异名字段(打印与 PDF 导出、旧灯箱六开关、旧点赞与默认数值四键)。
// 旧 Jio 私有 / 异名字段(打印与 PDF 导出、旧灯箱六开关、旧点赞与默认数值四键)。
const LEGACY_KEYS = [
'feature_print',
'feature_pdf_export',
@@ -194,7 +194,7 @@ test('旧字段在 schema 与运行时中均已移除(无双路)', () => {
assert.ok(!runtime.includes(legacy), `PostDisplay 残留旧字段: ${legacy}`);
assert.ok(!js.includes(legacy), `feature.js 残留旧字段: ${legacy}`);
assert.ok(!pageJs.includes(legacy), `page.js 残留旧字段: ${legacy}`);
assert.ok(!frontendTags.includes(legacy), `aksrt-frontend-tags 残留旧字段: ${legacy}`);
assert.ok(!frontendTags.includes(legacy), `jio-frontend-tags 残留旧字段: ${legacy}`);
}
assert.ok(!scripts1b.includes('needs_print'), 'ScriptsPart1b 残留 needs_print 分支');
assert.ok(!scripts1c.includes('needs_print'), 'ScriptsPart1c 残留 needs_print 需求');
@@ -228,5 +228,5 @@ test('运行时只读取新键并接入灯箱/目录树/预置参数', () => {
// 预置参数:新建文章随机填入。
assert.match(coreHooks, /save_post/);
assert.match(coreHooks, /POST_VIEW_COUNT_KEY/);
assert.match(coreHooks, /_aksrt_like_count/);
assert.match(coreHooks, /_jio_like_count/);
});
@@ -18,7 +18,7 @@ const php = read('src/Admin/Schema/SchemaPostList.php');
const postDisplay = read('src/Settings/PostDisplay.php');
const settings = read('src/Settings/Settings.php');
const termMeta = read('src/Core/TermMeta.php');
const filtersHelper = read('inc/helpers/aksrt-archive-filters.php');
const filtersHelper = read('inc/helpers/jio-archive-filters.php');
const categoryTpl = read('category.php');
const tagTpl = read('tag.php');
const listJs = read('assets/js/runtime/main.js');
@@ -26,7 +26,7 @@ const globalController = read('src/Admin/GlobalController.php');
// 参考主题字段键(顺序即参考主题定义顺序)。
const REFERENCE_KEYS = ['custom_filter_show', 'custom_filter', 'cat_custom_filter'];
// AKSRT 私有旧字段(参考主题无此配置,须移除避免双路)。
// Jio 私有旧字段(参考主题无此配置,须移除避免双路)。
const LEGACY_KEYS = [
'filter_enabled',
'filter_categories',
@@ -149,17 +149,17 @@ test('前台链路:覆盖与门禁(参考主题 zib-category.php 语义)',
test('前台链路:维度筛选渲染与查询过滤', () => {
// 渲染:维度 chip 组(全部 chip 参考主题 focus 语义)。
assert.match(filtersHelper, /function aksrt_archive_build_custom_filter_groups\(/);
assert.match(filtersHelper, /function jio_archive_build_custom_filter_groups\(/);
assert.match(filtersHelper, /ajax-replace="true" class="ajax-next/);
assert.match(filtersHelper, /\$keys !== 'all' && !in_array\(\$dim_key, \(array\) \$keys, true\)/);
assert.match(categoryTpl, /aksrt_archive_build_custom_filter_groups\(/);
assert.match(tagTpl, /aksrt_archive_build_custom_filter_groups\(/);
// 查询:_aksrt_cf_{key} meta_query LIKE(主查询 + AJAX 端点)。
assert.match(termMeta, /'_aksrt_cf_' \. \$dim_key/);
assert.match(categoryTpl, /jio_archive_build_custom_filter_groups\(/);
assert.match(tagTpl, /jio_archive_build_custom_filter_groups\(/);
// 查询:_jio_cf_{key} meta_query LIKE(主查询 + AJAX 端点)。
assert.match(termMeta, /'_jio_cf_' \. \$dim_key/);
assert.match(termMeta, /'compare'\s*=>\s*'LIKE'/);
// Category filtering uses the same HTML AJAX pipeline as the homepage.
assert.doesNotMatch(globalController, /rest_archive_posts|\/archive\/posts/);
assert.match(listJs, /win-ajax-replace/);
assert.match(listJs, /post_ajax/);
assert.match(filtersHelper, /'key'\s*=>\s*'_aksrt_cf_|\$dim_key/);
assert.match(filtersHelper, /'key'\s*=>\s*'_jio_cf_|\$dim_key/);
});
+2 -2
View File
@@ -110,7 +110,7 @@ test('每页条数默认 20 来自共享常量,且无「每页显示」控件'
for (const [key, src] of Object.entries(pages)) {
assert.doesNotMatch(src, /per_page_selector/, `${labels[key]} 出现 per_page 选择器`);
assert.doesNotMatch(src, /PER_PAGE_CHOICES/, `${labels[key]} 出现 PER_PAGE_CHOICES`);
assert.doesNotMatch(src, /aksrt-perpage-form/, `${labels[key]} 出现每页条数表单`);
assert.doesNotMatch(src, /jio-perpage-form/, `${labels[key]} 出现每页条数表单`);
assert.doesNotMatch(src, /name="ice_perpage"/, `${labels[key]} 出现 ice_perpage 输入框`);
assert.doesNotMatch(src, /order-header-right/, `${labels[key]} 出现工具栏右侧每页控件`);
}
@@ -191,7 +191,7 @@ test('提现审核不把一次性动作参数带进翻页链接', () => {
});
test('共享助手自身:页码夹取、四按钮标记与显式基准 URL', () => {
assert.match(helper, /namespace AKSRT\\Admin\\Native;/);
assert.match(helper, /namespace Jio\\Admin\\Native;/);
assert.match(helper, /defined\('ABSPATH'\) \|\| exit;/);
// 总数为 0 或每页条数非法时不渲染;单页照样渲染(与原版 render_pagenavi 一致)。
@@ -12,10 +12,10 @@
* post_front_content/after_content、post_related_*;
* 3) 幻灯片 fieldset 全字段及嵌套依赖(direction/scale_height/auto_height 组合)
* 对齐参考主题 CFS_Module::slide;
* 4) 旧 AKSRT 私有/异名字段(阅读进度条、面包屑末项文本、作者卡片配置、收藏开关、
* 4) 旧 Jio 私有/异名字段(阅读进度条、面包屑末项文本、作者卡片配置、收藏开关、
* share_s 与 post_rewards_*、旧封面键)已彻底移除,无双路兼容;
* 5) 运行时(PostDisplay::get_post_page_settings)只读取新键:幻灯片配置归一化并
* 接线 aksrt_render_slider,p_indent 接线容器类,breadcrumbs_home_text 为
* 接线 jio_render_slider,p_indent 接线容器类,breadcrumbs_home_text 为
* switcher 布尔语义(true = 用「首页」替代网站名称)。
*/
@@ -30,8 +30,8 @@ const js = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/pos
const runtime = fs.readFileSync(path.join(root, 'src/Settings/PostDisplay.php'), 'utf8');
const facadeBulk = fs.readFileSync(path.join(root, 'src/Settings/FacadeBulk.php'), 'utf8');
const metaBox = fs.readFileSync(path.join(root, 'src/Core/PostMetaBox.php'), 'utf8');
const frontendTags = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-frontend-tags.php'), 'utf8');
const singleRender = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-single-render.php'), 'utf8');
const frontendTags = fs.readFileSync(path.join(root, 'inc/helpers/jio-frontend-tags.php'), 'utf8');
const singleRender = fs.readFileSync(path.join(root, 'inc/helpers/jio-single-render.php'), 'utf8');
const contentCss = fs.readFileSync(path.join(root, 'assets/css/frontend/single-post/content.css'), 'utf8');
const shareJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/global/share.js'), 'utf8');
const sharePhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/GlobalSchema.php'), 'utf8');
@@ -104,7 +104,7 @@ const PAGE_FIELD_SEQUENCE = [
'post_related_orderby',
];
// 旧 AKSRT 私有/异名字段:已迁移(share_s / post_rewards_*)或无参考对应项被移除。
// 旧 Jio 私有/异名字段:已迁移(share_s / post_rewards_*)或无参考对应项被移除。
const LEGACY_KEYS = [
'post_reading_progress',
'post_author_card_limit',
@@ -271,7 +271,7 @@ test('旧字段在 schema 与运行时中均已移除(无双路)', () => {
assert.ok(!pagePhp.includes(`'breadcrumbs_single_text'`), 'page section 残留 breadcrumbs_single_text');
assert.ok(!js.includes(`key: 'breadcrumbs_single_text'`), 'page.js 残留 breadcrumbs_single_text');
assert.match(runtime, /'breadcrumbs_single_text'\s*=>\s*true/);
// aksrt-frontend-tags 不再有 breadcrumbs_home_text 文本兼容(switcher 布尔语义)。
// jio-frontend-tags 不再有 breadcrumbs_home_text 文本兼容(switcher 布尔语义)。
assert.ok(!frontendTags.includes("breadcrumbs_home'] = (string) $settings['breadcrumbs_home_text'"), 'frontend-tags 残留 breadcrumbs_home_text 文本兼容');
});
@@ -318,7 +318,7 @@ test('运行时只读取新键并接线封面/缩进/版权/一言', () => {
});
test('前台链路:幻灯片配置接线渲染器,p_indent 接线容器与 CSS', () => {
// 幻灯片配置接入 aksrt_render_slider(effect/direction/spaceBetween/speed/interval)。
// 幻灯片配置接入 jio_render_slider(effect/direction/spaceBetween/speed/interval)。
assert.match(singleRender, /article_slide_cover_option/);
assert.match(singleRender, /'effect'\s*=>\s*\(string\) \(\$slide_option\['effect'\] \?\? 'slide'\)/);
assert.match(singleRender, /'direction'\s*=>\s*\(string\) \(\$slide_option\['direction'\] \?\? 'horizontal'\)/);
@@ -11,7 +11,7 @@
* 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、
* 4) 旧 Jio 私有字段(front_submit_*、post_article_title_strlen_limit_min/max、
* post_article_pay_price_min/max)已彻底移除,无双路兼容;
* 5) 运行时(PostDisplay::get_submit_settings)只读取新键。
*/
+30 -30
View File
@@ -42,7 +42,7 @@ namespace {
}
}
namespace AKSRT\Ticket {
namespace Jio\Ticket {
final class TicketSettings {
public static bool $order_link_enabled = false;
public static function order_link_enabled(): bool { return self::$order_link_enabled; }
@@ -131,7 +131,7 @@ namespace AKSRT\Ticket {
}
}
namespace AKSRT\Attachment {
namespace Jio\Attachment {
final class PendingUploads {
public static function claim_urls(array $attachment, int $user_id): void {}
}
@@ -148,45 +148,45 @@ namespace {
}
};
$reset = static function (): void {
\AKSRT\Ticket\TicketRepository::$tickets = [
\Jio\Ticket\TicketRepository::$tickets = [
1 => ['id' => 1, 'status' => 'handling', 'ticket_type_id' => 3, 'admin_id' => 11],
2 => ['id' => 2, 'status' => 'closed', 'ticket_type_id' => 3, 'admin_id' => 11],
3 => ['id' => 3, 'status' => 'handling', 'ticket_type_id' => 3, 'admin_id' => 11],
];
\AKSRT\Ticket\TicketRepository::$creates = [];
\AKSRT\Ticket\TicketRepository::$updates = [];
\AKSRT\Ticket\TicketNotifier::$closed = [];
\AKSRT\Ticket\TicketNotifier::$user_replied = [];
\AKSRT\Ticket\TicketNoteService::$created = [];
\AKSRT\Ticket\TicketSettings::$order_link_enabled = false;
\Jio\Ticket\TicketRepository::$creates = [];
\Jio\Ticket\TicketRepository::$updates = [];
\Jio\Ticket\TicketNotifier::$closed = [];
\Jio\Ticket\TicketNotifier::$user_replied = [];
\Jio\Ticket\TicketNoteService::$created = [];
\Jio\Ticket\TicketSettings::$order_link_enabled = false;
$GLOBALS['ticket_rest_user_can_calls'] = [];
};
$admin = new \AKSRT\Ticket\TicketRestAdmin();
$admin = new \Jio\Ticket\TicketRestAdmin();
$error_code = static fn($value): string => $value instanceof \WP_Error ? $value->code : '';
// Explicit admin_id=0 clears assignment and status updates always touch updated_at.
$reset();
$response = $admin->set_status(new \WP_REST_Request(['id' => 1, 'status' => 'handling', 'admin_id' => 0]));
$saved = \AKSRT\Ticket\TicketRepository::$tickets[1] ?? [];
$saved = \Jio\Ticket\TicketRepository::$tickets[1] ?? [];
$check('解除指派接受 admin_id=0', !\is_wp_error($response) && ($saved['admin_id'] ?? null) === 0);
$check('状态更新写入 updated_at', ($saved['updated_at'] ?? '') === '2026-10-31 12:34:56');
// Invalid department returns an explicit error and performs no write.
$reset();
$response = $admin->set_status(new \WP_REST_Request(['id' => 1, 'status' => 'handling', 'ticket_type_id' => 999]));
$check('非法部门返回明确错误', $error_code($response) === 'aksrt_ticket_type');
$check('非法部门不写入任何字段', \AKSRT\Ticket\TicketRepository::$updates === []);
$check('非法部门返回明确错误', $error_code($response) === 'jio_ticket_type');
$check('非法部门不写入任何字段', \Jio\Ticket\TicketRepository::$updates === []);
// Existing assignees may lose admin capability; retaining the same ID isn't a new assignment.
$reset();
\AKSRT\Ticket\TicketRepository::$tickets[1]['admin_id'] = 12;
\Jio\Ticket\TicketRepository::$tickets[1]['admin_id'] = 12;
$response = $admin->set_status(new \WP_REST_Request([
'id' => 1,
'status' => 'admin_replied',
'ticket_type_id' => 4,
'admin_id' => 12,
]));
$saved = \AKSRT\Ticket\TicketRepository::$tickets[1] ?? [];
$saved = \Jio\Ticket\TicketRepository::$tickets[1] ?? [];
$check('保留已失去管理员权限的原跟进人仍可更新状态和部门', !\is_wp_error($response)
&& ($saved['status'] ?? null) === 'admin_replied'
&& ($saved['ticket_type_id'] ?? null) === 4
@@ -201,8 +201,8 @@ namespace {
'ticket_type_id' => 4,
'admin_id' => 12,
]));
$check('无 manage_options 的用户不能被指派', $error_code($response) === 'aksrt_ticket_admin');
$check('管理员校验失败时部门也不被部分写入', \AKSRT\Ticket\TicketRepository::$updates === []);
$check('无 manage_options 的用户不能被指派', $error_code($response) === 'jio_ticket_admin');
$check('管理员校验失败时部门也不被部分写入', \Jio\Ticket\TicketRepository::$updates === []);
$check('目标管理员必须通过 manage_options 校验', in_array([12, 'manage_options'], $GLOBALS['ticket_rest_user_can_calls'], true));
$reset();
@@ -211,13 +211,13 @@ namespace {
'status' => 'handling',
'admin_id' => 777,
]));
$check('不存在的管理员返回明确错误', $error_code($response) === 'aksrt_ticket_admin');
$check('不存在的管理员返回明确错误', $error_code($response) === 'jio_ticket_admin');
$check('不存在的管理员不会触发写入', \AKSRT\Ticket\TicketRepository::$updates === []);
$check('不存在的管理员不会触发写入', \Jio\Ticket\TicketRepository::$updates === []);
$reset();
\AKSRT\Ticket\TicketRepository::$tickets[1]['admin_id'] = 12;
\Jio\Ticket\TicketRepository::$tickets[1]['admin_id'] = 12;
$response = $admin->set_status(new \WP_REST_Request([
'id' => 1,
'status' => 'handling',
@@ -225,18 +225,18 @@ namespace {
]));
$check('具备 manage_options 的管理员可被指派', !\is_wp_error($response)
&& in_array([11, 'manage_options'], $GLOBALS['ticket_rest_user_can_calls'], true)
&& (\AKSRT\Ticket\TicketRepository::$tickets[1]['admin_id'] ?? null) === 11);
&& (\Jio\Ticket\TicketRepository::$tickets[1]['admin_id'] ?? null) === 11);
// Only the first open -> closed transition sends a close notification.
$reset();
$first_close = $admin->set_status(new \WP_REST_Request(['id' => 3, 'status' => 'closed']));
$second_close = $admin->set_status(new \WP_REST_Request(['id' => 3, 'status' => 'closed']));
$check('首次关闭成功', !\is_wp_error($first_close));
$check('同状态再次提交不重发关闭通知', count(\AKSRT\Ticket\TicketNotifier::$closed) === 1);
$check('同状态再次提交不重发关闭通知', count(\Jio\Ticket\TicketNotifier::$closed) === 1);
// Order association follows the feature switch.
$reset();
\AKSRT\Ticket\TicketSettings::$order_link_enabled = false;
\Jio\Ticket\TicketSettings::$order_link_enabled = false;
$admin->create_ticket(new \WP_REST_Request([
'user_id' => 10,
'title' => '代客提交标题',
@@ -244,9 +244,9 @@ namespace {
'content' => '这是问题描述内容',
'order_number' => 'ORD-1',
]));
$check('关闭订单关联时忽略订单号', (\AKSRT\Ticket\TicketRepository::$creates[0]['order_number'] ?? null) === '');
$check('关闭订单关联时忽略订单号', (\Jio\Ticket\TicketRepository::$creates[0]['order_number'] ?? null) === '');
\AKSRT\Ticket\TicketSettings::$order_link_enabled = true;
\Jio\Ticket\TicketSettings::$order_link_enabled = true;
$admin->create_ticket(new \WP_REST_Request([
'user_id' => 10,
'title' => '代客提交标题',
@@ -254,7 +254,7 @@ namespace {
'content' => '这是问题描述内容',
'order_number' => '<b>ORD-2</b>',
]));
$check('开启订单关联时保存已消毒订单号', (\AKSRT\Ticket\TicketRepository::$creates[1]['order_number'] ?? null) === 'ORD-2');
$check('开启订单关联时保存已消毒订单号', (\Jio\Ticket\TicketRepository::$creates[1]['order_number'] ?? null) === 'ORD-2');
// Internal note writes enforce existence, trim, non-empty and configured maximum length.
$reset();
@@ -262,10 +262,10 @@ namespace {
$empty = $admin->create_note(new \WP_REST_Request(['id' => 1, 'content' => " \n\t "]));
$long = $admin->create_note(new \WP_REST_Request(['id' => 1, 'content' => '123456']));
$valid = $admin->create_note(new \WP_REST_Request(['id' => 1, 'content' => ' memo ']));
$created = \AKSRT\Ticket\TicketNoteService::$created;
$check('备注工单不存在时返回 404', $error_code($missing) === 'aksrt_ticket_missing');
$check('备注拒绝空内容', $error_code($empty) === 'aksrt_ticket_note_content');
$check('备注拒绝超长内容', $error_code($long) === 'aksrt_ticket_note_content');
$created = \Jio\Ticket\TicketNoteService::$created;
$check('备注工单不存在时返回 404', $error_code($missing) === 'jio_ticket_missing');
$check('备注拒绝空内容', $error_code($empty) === 'jio_ticket_note_content');
$check('备注拒绝超长内容', $error_code($long) === 'jio_ticket_note_content');
$check('备注 trim 后保存', !\is_wp_error($valid) && ($created[0]['content'] ?? null) === 'memo');
$check('无效备注未调用创建服务', count($created) === 1);
+12 -12
View File
@@ -22,7 +22,7 @@ namespace {
function wp_get_current_user() { return (object) ['ID' => 7, 'display_name' => 'Operator']; }
function get_users($args) { return [(object) ['ID' => 7, 'display_name' => 'Operator'], (object) ['ID' => 8, 'display_name' => 'Second']]; }
function get_edit_user_link($id) { return admin_url('user-edit.php?user_id=' . $id); }
function aksrt_get_avatar_url($id, $size) { return 'https://example.test/avatar/' . $id; }
function jio_get_avatar_url($id, $size) { return 'https://example.test/avatar/' . $id; }
function add_query_arg($args, $url, $base = null) {
if (!is_array($args)) { $args = [$args => $url]; $url = $base; }
$parts = parse_url($url);
@@ -30,7 +30,7 @@ namespace {
return $parts['scheme'] . '://' . $parts['host'] . ($parts['path'] ?? '') . '?' . http_build_query(array_merge($query, $args));
}
}
namespace AKSRT\Ticket {
namespace Jio\Ticket {
class TicketSettings {
public static bool $orders = true;
public static function title_max() { return 100; }
@@ -94,8 +94,8 @@ namespace AKSRT\Ticket {
}
}
}
namespace AKSRT\Core {
class DBMigrator { public static function table($name) { return 'wp_aksrt_' . $name; } }
namespace Jio\Core {
class DBMigrator { public static function table($name) { return 'wp_jio_' . $name; } }
}
namespace {
$root = dirname(__DIR__, 2);
@@ -104,12 +104,12 @@ namespace {
}
require $root . '/src/Ticket/TicketQuery.php';
class TicketPage {
use \AKSRT\Admin\Native\UsersMenusTicketsCenter;
use \Jio\Admin\Native\UsersMenusTicketsCenter;
public const CAPABILITY = 'manage_options';
private function ensure_cap() {}
}
class QueryProbe {
use \AKSRT\Ticket\TicketQuery;
use \Jio\Ticket\TicketQuery;
public function where($args) { return $this->ticket_where($args, true); }
public function counts($args) { return $this->ticket_statistic($args, true); }
}
@@ -126,20 +126,20 @@ namespace {
$page->render_tickets_page();
return (string) ob_get_clean();
}
$repo = \AKSRT\Ticket\TicketRepository::class;
$repo = \Jio\Ticket\TicketRepository::class;
$page = new TicketPage();
$list = render_page($page, ['status' => 'handling', 'ticket_type_id' => 1, 's' => 'needle', 'paged' => 99]);
$initial = ['queries' => $repo::$queries, 'counts' => $repo::$counts, 'detail_reads' => $repo::$reads, 'reply_reads' => $repo::$replies_read];
$list_queries = count($repo::$queries);
$open = render_page($page, ['id' => 21, 'status' => 'pending', 's' => 'not in list', 'paged' => 3, 'aksrt_ticket_msg' => 'reply']);
$open = render_page($page, ['id' => 21, 'status' => 'pending', 's' => 'not in list', 'paged' => 3, 'jio_ticket_msg' => 'reply']);
$closed = render_page($page, ['id' => 99]);
$missing = render_page($page, ['id' => 404]);
$detail_list_queries = count($repo::$queries) - $list_queries;
$bogus_notice = render_page($page, ['id' => 21, 'aksrt_ticket_msg' => '<script>']);
\AKSRT\Ticket\TicketTypeService::$empty = true;
$bogus_notice = render_page($page, ['id' => 21, 'jio_ticket_msg' => '<script>']);
\Jio\Ticket\TicketTypeService::$empty = true;
$no_types = render_page($page, []);
\AKSRT\Ticket\TicketTypeService::$empty = false;
\AKSRT\Ticket\TicketSettings::$orders = false;
\Jio\Ticket\TicketTypeService::$empty = false;
\Jio\Ticket\TicketSettings::$orders = false;
$no_orders = render_page($page, []);
$GLOBALS['wpdb'] = new FakeDb();
$query = new QueryProbe();
+35 -35
View File
@@ -16,10 +16,10 @@ function rendered() {
test('list view links rows to the workspace and offers creation only as a dialog', () => {
const { list, initial } = rendered();
assert.equal((list.match(/<dialog /g) || []).length, 1, 'only the create dialog');
assert.match(list, /aksrt-native-header__actions[^]*data-aksrt-dialog-open="aksrt-ticket-create-dialog"/);
assert.match(list, /jio-native-header__actions[^]*data-jio-dialog-open="jio-ticket-create-dialog"/);
assert.doesNotMatch(list, /view=new/);
assert.match(list, /<a class="row-title" href="[^"]*page=aksrt_tickets&amp;id=21&amp;status=handling&amp;ticket_type_id=1&amp;s=needle&amp;paged=2"/);
assert.doesNotMatch(list, /data-aksrt-ticket-detail=|data-aksrt-ticket-form="(?:reply|status|note)"/);
assert.match(list, /<a class="row-title" href="[^"]*page=jio_tickets&amp;id=21&amp;status=handling&amp;ticket_type_id=1&amp;s=needle&amp;paged=2"/);
assert.doesNotMatch(list, /data-jio-ticket-detail=|data-jio-ticket-form="(?:reply|status|note)"/);
assert.equal(initial.detail_reads, 0);
assert.equal(initial.reply_reads, 0);
assert.equal(initial.queries.at(-1).offset, 20);
@@ -31,31 +31,31 @@ test('list view links rows to the workspace and offers creation only as a dialog
test('workspace loads by ID independent of list filters and keeps the list context for going back', () => {
const { open, closed, detail_list_queries: listQueries } = rendered();
assert.equal(listQueries, 0, 'workspace must not query the list');
assert.match(open, /<h1 class="aksrt-native-header__title">[^]*Question &lt;svg onload=alert\(1\)&gt;<\/h1>/);
assert.match(open, /href="[^"]*page=aksrt_tickets&amp;status=pending&amp;s=not\+in\+list&amp;paged=3"[^>]*>[^]*返回工单列表/);
assert.match(open, /data-aksrt-ticket-detail="21"/);
for (const action of ['reply', 'status', 'note']) assert.ok(open.includes(`data-aksrt-ticket-form="${action}"`));
assert.match(open, /action="https:\/\/example.test\/wp-json\/aksrt\/v1\/admin\/tickets\/21\/notes"/);
assert.match(open, /data-aksrt-ticket-close-confirm/);
assert.match(open, /<h1 class="jio-native-header__title">[^]*Question &lt;svg onload=alert\(1\)&gt;<\/h1>/);
assert.match(open, /href="[^"]*page=jio_tickets&amp;status=pending&amp;s=not\+in\+list&amp;paged=3"[^>]*>[^]*返回工单列表/);
assert.match(open, /data-jio-ticket-detail="21"/);
for (const action of ['reply', 'status', 'note']) assert.ok(open.includes(`data-jio-ticket-form="${action}"`));
assert.match(open, /action="https:\/\/example.test\/wp-json\/jio\/v1\/admin\/tickets\/21\/notes"/);
assert.match(open, /data-jio-ticket-close-confirm/);
assert.match(open, /value="closed" data-closed="1"/);
assert.match(open, /<dt>工单号<\/dt><dd>TK-00021<\/dd>/);
assert.match(open, /data-aksrt-ticket-feedback/);
assert.match(closed, /data-aksrt-ticket-detail="99"/);
assert.match(closed, /aksrt-ticket-detail__closed/);
assert.match(closed, /data-aksrt-ticket-form="note"/);
assert.doesNotMatch(closed, /data-aksrt-ticket-form="(?:reply|status)"/);
assert.match(open, /data-jio-ticket-feedback/);
assert.match(closed, /data-jio-ticket-detail="99"/);
assert.match(closed, /jio-ticket-detail__closed/);
assert.match(closed, /data-jio-ticket-form="note"/);
assert.doesNotMatch(closed, /data-jio-ticket-form="(?:reply|status)"/);
// 回复框是会话卡片的底部,不是独立卡片;工作区唯一的弹窗是内部备注。
const conversation = open.match(/<section class="aksrt-ticket-conversation"[^]*?<\/section>/)?.[0] || '';
assert.match(conversation, /aksrt-ticket-conversation__scroll[^]*data-aksrt-ticket-form="reply"/);
assert.match(conversation, /data-aksrt-ticket-send-shortcut/);
const conversation = open.match(/<section class="jio-ticket-conversation"[^]*?<\/section>/)?.[0] || '';
assert.match(conversation, /jio-ticket-conversation__scroll[^]*data-jio-ticket-form="reply"/);
assert.match(conversation, /data-jio-ticket-send-shortcut/);
assert.match(conversation, /<button type="submit" class="button button-primary">[^]*发送回复<\/button>/);
for (const html of [open, closed]) {
assert.equal((html.match(/<dialog /g) || []).length, 1);
assert.match(html, /<dialog id="aksrt-ticket-note-dialog"[^]*data-aksrt-ticket-form="note"[^]*<\/dialog>/);
assert.match(html, /data-aksrt-dialog-open="aksrt-ticket-note-dialog"/);
assert.match(html, /<dialog id="jio-ticket-note-dialog"[^]*data-jio-ticket-form="note"[^]*<\/dialog>/);
assert.match(html, /data-jio-dialog-open="jio-ticket-note-dialog"/);
}
const sidebar = open.match(/<aside\b[^]*?<\/aside>/)?.[0] || '';
assert.doesNotMatch(sidebar, /<textarea|data-aksrt-ticket-form="note"/, 'note input lives only in the dialog');
assert.doesNotMatch(sidebar, /<textarea|data-jio-ticket-form="note"/, 'note input lives only in the dialog');
});
test('result codes render as one native notice and unknown codes are ignored', () => {
@@ -70,18 +70,18 @@ test('ticket information owns the only status editor and closed tickets stay rea
const { open, closed } = rendered();
const sidebarCards = html => {
const sidebar = html.match(/<aside\b[^>]*>([\s\S]*?)<\/aside>/)?.[1] || '';
return [...sidebar.matchAll(/<section class="aksrt-ticket-detail__card(?: [^"]+)?">([\s\S]*?)<\/section>/g)].map(match => match[1]);
return [...sidebar.matchAll(/<section class="jio-ticket-detail__card(?: [^"]+)?">([\s\S]*?)<\/section>/g)].map(match => match[1]);
};
const cards = sidebarCards(open);
assert.equal(cards.length, 2, 'only ticket information and internal notes remain');
const info = cards[0];
assert.match(info, /<h3>工单信息<\/h3>/);
assert.match(info, /data-aksrt-ticket-form="status"/);
assert.match(info, /data-aksrt-ticket-assign/);
assert.match(info, /data-jio-ticket-form="status"/);
assert.match(info, /data-jio-ticket-assign/);
assert.match(info, /保存工单信息/);
assert.doesNotMatch(info, /aksrt-ticket-badge/);
assert.doesNotMatch(info, /jio-ticket-badge/);
for (const [name, label] of [['status', '状态'], ['ticket_type_id', '部门'], ['admin_id', '跟进人']]) {
const id = `aksrt-ticket-${name}-21`;
const id = `jio-ticket-${name}-21`;
assert.ok(info.includes(`<label for="${id}">${label}</label>`));
assert.ok(info.includes(`<select id="${id}" name="${name}"`));
assert.equal((open.match(new RegExp(`name="${name}"`, 'g')) || []).length, 1);
@@ -89,25 +89,25 @@ test('ticket information owns the only status editor and closed tickets stay rea
assert.match(cards[1], /内部备注/);
const closedInfo = sidebarCards(closed)[0];
assert.match(closedInfo, /<h3>工单信息<\/h3>/);
assert.match(closedInfo, /aksrt-ticket-badge/);
assert.doesNotMatch(closedInfo, /<form|<select|data-aksrt-ticket-assign|保存工单信息/);
assert.match(closedInfo, /jio-ticket-badge/);
assert.doesNotMatch(closedInfo, /<form|<select|data-jio-ticket-assign|保存工单信息/);
});
test('missing ticket renders an empty state with a way back and no forms', () => {
const { missing } = rendered();
assert.match(missing, /工单不存在/);
assert.match(missing, /返回工单列表/);
assert.doesNotMatch(missing, /data-aksrt-ticket-form|data-aksrt-ticket-detail/);
assert.doesNotMatch(missing, /data-jio-ticket-form|data-jio-ticket-detail/);
});
test('create dialog is a labelled REST form with its own feedback and close controls', () => {
const { list } = rendered();
const dialog = list.match(/<dialog id="aksrt-ticket-create-dialog"[^]*?<\/dialog>/)?.[0] || '';
assert.match(dialog, /aria-labelledby="aksrt-ticket-create-title"/);
assert.match(dialog, /<form method="post" action="https:\/\/example.test\/wp-json\/aksrt\/v1\/admin\/tickets" class="aksrt-ticket-dialog__frame" data-aksrt-ticket-form="create">/);
const dialog = list.match(/<dialog id="jio-ticket-create-dialog"[^]*?<\/dialog>/)?.[0] || '';
assert.match(dialog, /aria-labelledby="jio-ticket-create-title"/);
assert.match(dialog, /<form method="post" action="https:\/\/example.test\/wp-json\/jio\/v1\/admin\/tickets" class="jio-ticket-dialog__frame" data-jio-ticket-form="create">/);
for (const name of ['user_id', 'ticket_type_id', 'title', 'order_number', 'content']) assert.ok(dialog.includes(`name="${name}"`), name);
assert.match(dialog, /data-aksrt-ticket-feedback/);
assert.equal((dialog.match(/data-aksrt-dialog-close/g) || []).length, 2, 'header X and footer cancel');
assert.match(dialog, /data-jio-ticket-feedback/);
assert.equal((dialog.match(/data-jio-dialog-close/g) || []).length, 2, 'header X and footer cancel');
});
test('real rendering escapes user content, quotes and metadata', () => {
@@ -125,9 +125,9 @@ test('real rendering escapes user content, quotes and metadata', () => {
test('missing departments and disabled order links have explicit creation states', () => {
const { no_types: noTypes, no_orders: noOrders } = rendered();
assert.match(noTypes, /尚未配置工单部门/);
assert.doesNotMatch(noTypes, /data-aksrt-ticket-form="create"/);
assert.doesNotMatch(noTypes, /data-jio-ticket-form="create"/);
assert.match(noTypes, /前往工单配置/);
assert.match(noOrders, /data-aksrt-ticket-form="create"/);
assert.match(noOrders, /data-jio-ticket-form="create"/);
assert.doesNotMatch(noOrders, /name="order_number"/);
});
+48 -48
View File
@@ -8,9 +8,9 @@ const vm = require('node:vm');
const scriptPath = path.resolve(__dirname, '../../assets/js/admin-tickets.js');
const strings = { saving: 'Saving', saveFailed: 'Save failed', uncertain: 'Unknown result', required: 'Required', tooLong: 'Too long', discard: 'Discard?' };
const REST = 'https://site.test/wp-json/aksrt/v1/admin/tickets';
const REST = 'https://site.test/wp-json/jio/v1/admin/tickets';
function makeRuntime({ href = 'https://site.test/wp-admin/users.php?page=aksrt_tickets&id=41', page = 'detail', storage = {} } = {}) {
function makeRuntime({ href = 'https://site.test/wp-admin/users.php?page=jio_tickets&id=41', page = 'detail', storage = {} } = {}) {
const ids = new Map();
const document = { activeElement: null };
@@ -134,69 +134,69 @@ function makeRuntime({ href = 'https://site.test/wp-admin/users.php?page=aksrt_t
};
function makeDialog(id) {
const dialog = el('dialog', { id, class: 'aksrt-ticket-dialog' }, root);
const dialog = el('dialog', { id, class: 'jio-ticket-dialog' }, root);
dialog.open = false;
dialog.showModal = () => { dialog.open = true; };
dialog.close = () => { if (!dialog.open) return; dialog.open = false; dialog.fire('close'); };
return dialog;
}
const root = new Element('div', { class: 'aksrt-tickets-admin' });
const feedback = el('div', { 'data-aksrt-ticket-feedback': '', hidden: '' }, root);
const root = new Element('div', { class: 'jio-tickets-admin' });
const feedback = el('div', { 'data-jio-ticket-feedback': '', hidden: '' }, root);
el('p', {}, feedback);
el('button', { type: 'button', 'data-aksrt-ticket-reload': '', hidden: '' }, feedback);
el('button', { type: 'button', 'data-jio-ticket-reload': '', hidden: '' }, feedback);
const nodes = { root, feedback };
if (page === 'detail') {
const detail = el('div', { 'data-aksrt-ticket-detail': '41' }, root);
nodes.latest = el('button', { type: 'button', 'data-aksrt-ticket-latest': '', hidden: '' }, detail);
nodes.conversation = el('div', { class: 'aksrt-ticket-conversation__scroll' }, detail);
const reply = nodes.reply = el('form', { action: `${REST}/41/reply`, 'data-aksrt-ticket-form': 'reply' }, detail);
nodes.replyText = el('textarea', { id: 'aksrt-ticket-reply-41', name: 'content', required: '', maxlength: '24', 'data-aksrt-ticket-editor': '' }, reply);
nodes.replyCount = el('span', { 'data-aksrt-ticket-counter': 'aksrt-ticket-reply-41' }, reply);
nodes.prereply = select(reply, { 'data-aksrt-ticket-prereply': 'aksrt-ticket-reply-41' }, [[''], ['short'], ['01234567890123456789012345']]);
const detail = el('div', { 'data-jio-ticket-detail': '41' }, root);
nodes.latest = el('button', { type: 'button', 'data-jio-ticket-latest': '', hidden: '' }, detail);
nodes.conversation = el('div', { class: 'jio-ticket-conversation__scroll' }, detail);
const reply = nodes.reply = el('form', { action: `${REST}/41/reply`, 'data-jio-ticket-form': 'reply' }, detail);
nodes.replyText = el('textarea', { id: 'jio-ticket-reply-41', name: 'content', required: '', maxlength: '24', 'data-jio-ticket-editor': '' }, reply);
nodes.replyCount = el('span', { 'data-jio-ticket-counter': 'jio-ticket-reply-41' }, reply);
nodes.prereply = select(reply, { 'data-jio-ticket-prereply': 'jio-ticket-reply-41' }, [[''], ['short'], ['01234567890123456789012345']]);
el('button', { type: 'submit' }, reply);
const section = el('section', {}, detail);
nodes.assign = el('button', { type: 'button', 'data-aksrt-ticket-assign': '' }, section);
const status = nodes.status = el('form', { action: `${REST}/41/status`, 'data-aksrt-ticket-form': 'status' }, section);
nodes.statusSelect = select(status, { name: 'status', 'data-aksrt-ticket-status': '' }, [['pending', { 'data-closed': '0' }], ['handling', { 'data-closed': '0' }], ['closed', { 'data-closed': '1' }]], 'pending');
nodes.assign = el('button', { type: 'button', 'data-jio-ticket-assign': '' }, section);
const status = nodes.status = el('form', { action: `${REST}/41/status`, 'data-jio-ticket-form': 'status' }, section);
nodes.statusSelect = select(status, { name: 'status', 'data-jio-ticket-status': '' }, [['pending', { 'data-closed': '0' }], ['handling', { 'data-closed': '0' }], ['closed', { 'data-closed': '1' }]], 'pending');
select(status, { name: 'ticket_type_id', required: '' }, [['3'], ['6']], '3');
nodes.admin = select(status, { name: 'admin_id' }, [['0'], ['7'], ['44']], '7');
nodes.warning = el('div', { 'data-aksrt-ticket-close-warning': '', hidden: '' }, status);
nodes.confirm = el('input', { type: 'checkbox', 'data-aksrt-ticket-close-confirm': '' }, nodes.warning);
nodes.warning = el('div', { 'data-jio-ticket-close-warning': '', hidden: '' }, status);
nodes.confirm = el('input', { type: 'checkbox', 'data-jio-ticket-close-confirm': '' }, nodes.warning);
nodes.statusSubmit = el('button', { type: 'submit' }, status);
nodes.noteOpener = el('button', { type: 'button', 'data-aksrt-dialog-open': 'aksrt-ticket-note-dialog' }, detail);
const noteDialog = nodes.noteDialog = makeDialog('aksrt-ticket-note-dialog');
const note = nodes.note = el('form', { action: `${REST}/41/notes`, 'data-aksrt-ticket-form': 'note' }, noteDialog);
nodes.noteClose = el('button', { type: 'button', 'data-aksrt-dialog-close': '' }, note);
nodes.noteFeedback = el('div', { 'data-aksrt-ticket-feedback': '', hidden: '' }, note);
nodes.noteOpener = el('button', { type: 'button', 'data-jio-dialog-open': 'jio-ticket-note-dialog' }, detail);
const noteDialog = nodes.noteDialog = makeDialog('jio-ticket-note-dialog');
const note = nodes.note = el('form', { action: `${REST}/41/notes`, 'data-jio-ticket-form': 'note' }, noteDialog);
nodes.noteClose = el('button', { type: 'button', 'data-jio-dialog-close': '' }, note);
nodes.noteFeedback = el('div', { 'data-jio-ticket-feedback': '', hidden: '' }, note);
el('p', {}, nodes.noteFeedback);
el('button', { type: 'button', 'data-aksrt-ticket-reload': '', hidden: '' }, nodes.noteFeedback);
nodes.noteText = el('textarea', { id: 'aksrt-ticket-note-41', name: 'content', required: '', autofocus: '', maxlength: '24', 'data-aksrt-ticket-editor': '' }, note);
el('button', { type: 'button', 'data-jio-ticket-reload': '', hidden: '' }, nodes.noteFeedback);
nodes.noteText = el('textarea', { id: 'jio-ticket-note-41', name: 'content', required: '', autofocus: '', maxlength: '24', 'data-jio-ticket-editor': '' }, note);
el('button', { type: 'submit' }, note);
note.reset = () => { nodes.noteText.value = ''; };
nodes.replyText.attrs['data-aksrt-ticket-send-shortcut'] = '';
nodes.replyText.attrs['data-jio-ticket-send-shortcut'] = '';
reply.requestSubmit = () => reply.fire('submit') && root.fire('submit', { target: reply });
} else {
nodes.opener = el('button', { type: 'button', 'data-aksrt-dialog-open': 'aksrt-ticket-create-dialog' }, root);
const dialog = nodes.dialog = makeDialog('aksrt-ticket-create-dialog');
const create = nodes.create = el('form', { action: REST, 'data-aksrt-ticket-form': 'create' }, dialog);
nodes.closeButton = el('button', { type: 'button', 'data-aksrt-dialog-close': '' }, create);
nodes.opener = el('button', { type: 'button', 'data-jio-dialog-open': 'jio-ticket-create-dialog' }, root);
const dialog = nodes.dialog = makeDialog('jio-ticket-create-dialog');
const create = nodes.create = el('form', { action: REST, 'data-jio-ticket-form': 'create' }, dialog);
nodes.closeButton = el('button', { type: 'button', 'data-jio-dialog-close': '' }, create);
el('input', { type: 'number', name: 'user_id', required: '' }, create);
select(create, { name: 'ticket_type_id', required: '' }, [[''], ['3']], '');
el('input', { type: 'text', name: 'title', required: '' }, create);
el('textarea', { id: 'aksrt-ticket-create-content', name: 'content', required: '', maxlength: '500', 'data-aksrt-ticket-editor': '' }, create);
el('textarea', { id: 'jio-ticket-create-content', name: 'content', required: '', maxlength: '500', 'data-jio-ticket-editor': '' }, create);
el('button', { type: 'submit' }, create);
create.reset = () => create.querySelectorAll('input, select, textarea').forEach((control) => { control.value = ''; });
}
document.querySelector = (selector) => (selector === '.aksrt-tickets-admin' ? root : null);
document.querySelector = (selector) => (selector === '.jio-tickets-admin' ? root : null);
document.getElementById = (id) => ids.get(id) || null;
const store = new Map(Object.entries(storage));
const window = new Element('window');
Object.assign(window, {
AKSRTAdminTickets: { restNonce: 'rest-nonce', currentUser: 44, strings },
JioAdminTickets: { restNonce: 'rest-nonce', currentUser: 44, strings },
location: { href, assigned: null, reloads: 0, assign(url) { this.assigned = url; }, reload() { this.reloads += 1; } },
history: { state: null, replaceState(_state, _title, url) { window.location.href = url; } },
sessionStorage: { getItem: (key) => store.get(key) ?? null, setItem: (key, value) => store.set(key, String(value)), removeItem: (key) => store.delete(key) },
@@ -218,7 +218,7 @@ const submit = (rt, form) => rt.root.fire('submit', { target: form });
const unload = (rt) => rt.window.fire('beforeunload', { returnValue: undefined });
test('a successful reply posts JSON to REST, then reloads the workspace with a result code and stashes other drafts', async () => {
const rt = makeRuntime({ href: 'https://site.test/wp-admin/users.php?page=aksrt_tickets&id=41&status=pending&paged=3' });
const rt = makeRuntime({ href: 'https://site.test/wp-admin/users.php?page=jio_tickets&id=41&status=pending&paged=3' });
type(rt, rt.noteText, 'pending note');
type(rt, rt.replyText, 'hello');
const event = submit(rt, rt.reply);
@@ -237,27 +237,27 @@ test('a successful reply posts JSON to REST, then reloads the workspace with a r
await tick();
const next = new URL(rt.window.location.assigned);
assert.equal(next.searchParams.get('id'), '41');
assert.equal(next.searchParams.get('aksrt_ticket_msg'), 'reply');
assert.equal(next.searchParams.get('jio_ticket_msg'), 'reply');
assert.equal(next.searchParams.get('status'), 'pending');
assert.equal(next.searchParams.get('paged'), '3');
assert.deepEqual(JSON.parse(rt.store.get('aksrt-ticket-drafts-41')), { note: { content: 'pending note' } });
assert.deepEqual(JSON.parse(rt.store.get('jio-ticket-drafts-41')), { note: { content: 'pending note' } });
assert.equal(unload(rt).defaultPrevented, false, 'navigation after save is not blocked');
});
test('after the reload, stashed drafts are restored once and the result code is stripped from the URL', () => {
const rt = makeRuntime({
href: 'https://site.test/wp-admin/users.php?page=aksrt_tickets&id=41&aksrt_ticket_msg=reply',
storage: { 'aksrt-ticket-drafts-41': JSON.stringify({ note: { content: 'keep me' } }) },
href: 'https://site.test/wp-admin/users.php?page=jio_tickets&id=41&jio_ticket_msg=reply',
storage: { 'jio-ticket-drafts-41': JSON.stringify({ note: { content: 'keep me' } }) },
});
assert.equal(rt.noteText.value, 'keep me');
assert.equal(rt.store.has('aksrt-ticket-drafts-41'), false);
assert.equal(new URL(rt.window.location.href).searchParams.has('aksrt_ticket_msg'), false);
assert.equal(rt.store.has('jio-ticket-drafts-41'), false);
assert.equal(new URL(rt.window.location.href).searchParams.has('jio_ticket_msg'), false);
assert.equal(new URL(rt.window.location.href).searchParams.get('id'), '41');
assert.equal(unload(rt).defaultPrevented, true, 'a restored draft still counts as unsaved');
});
test('create dialog opens from the header button, guards dirty close, resets and returns focus', () => {
const rt = makeRuntime({ page: 'create', href: 'https://site.test/wp-admin/users.php?page=aksrt_tickets' });
const rt = makeRuntime({ page: 'create', href: 'https://site.test/wp-admin/users.php?page=jio_tickets' });
rt.opener.isConnected = true;
rt.root.fire('click', { target: rt.opener });
assert.equal(rt.dialog.open, true);
@@ -286,7 +286,7 @@ test('create dialog opens from the header button, guards dirty close, resets and
});
test('create posts to the collection route and opens the new ticket workspace with the list context kept', async () => {
const rt = makeRuntime({ page: 'create', href: 'https://site.test/wp-admin/users.php?page=aksrt_tickets&status=pending&paged=2' });
const rt = makeRuntime({ page: 'create', href: 'https://site.test/wp-admin/users.php?page=jio_tickets&status=pending&paged=2' });
rt.root.fire('click', { target: rt.opener });
const form = rt.create;
form.querySelector('[name="user_id"]').value = '12';
@@ -301,7 +301,7 @@ test('create posts to the collection route and opens the new ticket workspace wi
const next = new URL(rt.window.location.assigned);
assert.equal(next.searchParams.has('view'), false);
assert.equal(next.searchParams.get('id'), '77');
assert.equal(next.searchParams.get('aksrt_ticket_msg'), 'created');
assert.equal(next.searchParams.get('jio_ticket_msg'), 'created');
assert.equal(next.searchParams.get('status'), 'pending');
assert.equal(next.searchParams.get('paged'), '2');
});
@@ -318,12 +318,12 @@ test('a confirmed validation error keeps the form editable and shows the server
const rt = makeRuntime();
type(rt, rt.replyText, 'hello');
submit(rt, rt.reply);
rt.fetchCalls[0].resolve(reply({ code: 'aksrt_ticket_reply_content', message: 'Bad content', data: { status: 400 } }, 400));
rt.fetchCalls[0].resolve(reply({ code: 'jio_ticket_reply_content', message: 'Bad content', data: { status: 400 } }, 400));
await tick();
assert.equal(rt.window.location.assigned, null);
assert.equal(rt.feedback.hidden, false);
assert.equal(rt.feedback.querySelector('p').textContent, 'Bad content');
assert.equal(rt.feedback.querySelector('[data-aksrt-ticket-reload]').hidden, true);
assert.equal(rt.feedback.querySelector('[data-jio-ticket-reload]').hidden, true);
assert.equal(rt.replyText.disabled, false);
assert.equal(rt.replyText.value, 'hello');
submit(rt, rt.reply);
@@ -332,8 +332,8 @@ test('a confirmed validation error keeps the form editable and shows the server
for (const [label, settle] of [
['an unknown network result', (call) => call.reject(new TypeError('network'))],
['a 409 state conflict', (call) => call.resolve(reply({ code: 'aksrt_ticket_closed', message: 'Closed', data: { status: 409 } }, 409))],
['a 404 missing ticket', (call) => call.resolve(reply({ code: 'aksrt_ticket_missing', message: 'Missing', data: { status: 404 } }, 404))],
['a 409 state conflict', (call) => call.resolve(reply({ code: 'jio_ticket_closed', message: 'Closed', data: { status: 409 } }, 409))],
['a 404 missing ticket', (call) => call.resolve(reply({ code: 'jio_ticket_missing', message: 'Missing', data: { status: 404 } }, 404))],
]) {
test(`${label} locks every write form and only offers a page reload`, async () => {
const rt = makeRuntime();
@@ -341,7 +341,7 @@ for (const [label, settle] of [
submit(rt, rt.reply);
settle(rt.fetchCalls[0]);
await tick();
const reload = rt.feedback.querySelector('[data-aksrt-ticket-reload]');
const reload = rt.feedback.querySelector('[data-jio-ticket-reload]');
assert.equal(reload.hidden, false);
for (const control of [rt.replyText, rt.noteText, rt.statusSelect, rt.statusSubmit, rt.assign]) assert.equal(control.disabled, true);
submit(rt, rt.note);
+5 -5
View File
@@ -42,7 +42,7 @@ namespace {
function get_option($k, $default = false) { return $GLOBALS['wechat_options'][$k] ?? $default; }
function update_option($k, $v) { $GLOBALS['wechat_options'][$k] = $v; return true; }
$GLOBALS['wechat_options'] = ['aksrt_wechat_settings' => [
$GLOBALS['wechat_options'] = ['jio_wechat_settings' => [
'enabled' => true,
'app_id' => 'wxAPPID',
'app_secret' => 'SECRET123',
@@ -56,7 +56,7 @@ namespace {
};
}
namespace AKSRT\Settings {
namespace Jio\Settings {
// Settings 层仅为薄委托,harness 直接对接真实 Integration。
class Settings {
public static function get_wechat_settings(): array { return Integration::get_wechat_settings(); }
@@ -83,11 +83,11 @@ namespace {
}
};
$stored = static function (): array {
$v = get_option('aksrt_wechat_settings', []);
$v = get_option('jio_wechat_settings', []);
return is_array($v) ? $v : [];
};
$controller = \AKSRT\Admin\UserController::get_instance();
$controller = \Jio\Admin\UserController::get_instance();
// 1) GET /wechat/status 返回完整状态形状(密钥掩码)。
$response = $controller->get_wechat_status(new WP_REST_Request());
@@ -135,7 +135,7 @@ namespace {
$check('未知键被忽略', !array_key_exists('evil', $stored()));
// 6) 未配置时空密钥可显式清空。
update_option('aksrt_wechat_settings', ['enabled' => true, 'app_id' => 'a', 'app_secret' => '', 'template_id' => '']);
update_option('jio_wechat_settings', ['enabled' => true, 'app_id' => 'a', 'app_secret' => '', 'template_id' => '']);
$controller->update_wechat_settings(new WP_REST_Request(['app_secret' => '']));
$check('空密钥可清存', ($stored()['app_secret'] ?? '') === '');
@@ -6,8 +6,8 @@
* 2) 状态读取委托 WeChatNotify::get_status(),写入委托 Settings::update_wechat_settings();
* 3) app_secret 掩码 *** 不覆盖已存密钥,输入经 sanitize_text_field 消毒;
* 4) PHP harness 驱动真实控制器验证上述行为;
* 5) aksrt_settings 上的微信通知死键(wechat_notify_* 等)已彻底移除,
* 活配置面仅剩 aksrt_wechat_settings(/users/wechat-template 自定义页)。
* 5) jio_settings 上的微信通知死键(wechat_notify_* 等)已彻底移除,
* 活配置面仅剩 jio_wechat_settings(/users/wechat-template 自定义页)。
*/
const assert = require('node:assert/strict');
@@ -22,7 +22,7 @@ const schemaUsers = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUser
const usersSchemaIndex = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/index.js'), 'utf8');
const usersSections = fs.readFileSync(path.join(root, 'assets/admin/src/constants/sections/users.js'), 'utf8');
// 死双路面:写进 aksrt_settings、全仓无消费方的微信通知键。
// 死双路面:写进 jio_settings、全仓无消费方的微信通知键。
const DEAD_WECHAT_KEYS = [
'wechat_notify_enabled',
'wechat_app_id',
@@ -66,7 +66,7 @@ test('微信设置行为 harness 通过', () => {
assert.match(output, /WeChat settings harness passed/);
});
test('死双路面(aksrt_settings 微信通知键)已彻底移除', () => {
test('死双路面(jio_settings 微信通知键)已彻底移除', () => {
for (const key of DEAD_WECHAT_KEYS) {
assert.ok(!schemaUsers.includes(key), `SchemaUsers 残留: ${key}`);
assert.ok(!usersSections.includes(key), `users 菜单常量残留: ${key}`);
@@ -76,7 +76,7 @@ test('死双路面(aksrt_settings 微信通知键)已彻底移除', () => {
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/schema/sections/users/wechat.js')), 'wechat.js 未删除');
});
test('活配置面保持唯一(aksrt_wechat_settings + 自定义页)', () => {
test('活配置面保持唯一(jio_wechat_settings + 自定义页)', () => {
assert.match(usersSections, /key: 'users-wechat-template'/);
assert.match(usersSections, /route: '\/users\/wechat-template'/);
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
+2 -2
View File
@@ -1,7 +1,7 @@
<?php
use AKSRT\Ledger\AssetAmount;
use AKSRT\Ledger\AssetType;
use Jio\Ledger\AssetAmount;
use Jio\Ledger\AssetType;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
use AKSRT\Finance\BusinessNumber;
use Jio\Finance\BusinessNumber;
use PHPUnit\Framework\TestCase;
final class BusinessNumberTest extends TestCase {
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
use AKSRT\Payment\CommissionLedger;
use Jio\Payment\CommissionLedger;
use PHPUnit\Framework\TestCase;
final class CommissionLedgerTest extends TestCase {
+13 -13
View File
@@ -1,7 +1,7 @@
<?php
use AKSRT\Core\DomainModel;
use AKSRT\Core\FrontSubmitPay;
use Jio\Core\DomainModel;
use Jio\Core\FrontSubmitPay;
use PHPUnit\Framework\TestCase;
require_once dirname(__DIR__, 2) . '/src/Core/DomainModel.php';
@@ -9,7 +9,7 @@ require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmitPay.php';
if (!function_exists('update_post_meta')) {
function update_post_meta(int $post_id, string $key, $value): void {
$GLOBALS['aksrt_test_post_meta'][$post_id][$key] = $value;
$GLOBALS['jio_test_post_meta'][$post_id][$key] = $value;
}
}
@@ -29,21 +29,21 @@ final class FrontSubmitPayTest extends TestCase {
private FrontSubmitPayHarness $submit;
protected function setUp(): void {
$GLOBALS['aksrt_test_post_meta'] = [];
$GLOBALS['jio_test_post_meta'] = [];
$this->submit = new FrontSubmitPayHarness();
}
public function test_manual_cash_price_only_requires_enabled_vip_level(): void {
$result = $this->submit->save(101, [
'pay_enabled' => '1',
'aksrt_pay' => [
'jio_pay' => [
'price' => '10.00',
'vip1_price' => '8.00',
],
], $this->settings(true, false));
self::assertTrue($result['success']);
$meta = $GLOBALS['aksrt_test_post_meta'][101][DomainModel::PAY_META_KEY];
$meta = $GLOBALS['jio_test_post_meta'][101][DomainModel::PAY_META_KEY];
self::assertSame('8.00', $meta['vip1_price']);
self::assertSame('0.00', $meta['vip2_price']);
}
@@ -51,7 +51,7 @@ final class FrontSubmitPayTest extends TestCase {
public function test_manual_points_price_only_requires_enabled_vip_level(): void {
$result = $this->submit->save(102, [
'pay_enabled' => '1',
'aksrt_pay' => [
'jio_pay' => [
'pay_modo' => 'points',
'points_price' => '100',
'vip2_points' => '50',
@@ -59,7 +59,7 @@ final class FrontSubmitPayTest extends TestCase {
], $this->settings(false, true));
self::assertTrue($result['success']);
$meta = $GLOBALS['aksrt_test_post_meta'][102][DomainModel::PAY_META_KEY];
$meta = $GLOBALS['jio_test_post_meta'][102][DomainModel::PAY_META_KEY];
self::assertSame(0, $meta['vip1_points']);
self::assertSame(50, $meta['vip2_points']);
}
@@ -67,7 +67,7 @@ final class FrontSubmitPayTest extends TestCase {
public function test_manual_price_rejects_missing_enabled_vip_level(): void {
$result = $this->submit->save(103, [
'pay_enabled' => '1',
'aksrt_pay' => ['price' => '10.00'],
'jio_pay' => ['price' => '10.00'],
], $this->settings(true, false));
self::assertFalse($result['success']);
@@ -77,11 +77,11 @@ final class FrontSubmitPayTest extends TestCase {
public function test_manual_price_does_not_require_disabled_vip_levels(): void {
$result = $this->submit->save(104, [
'pay_enabled' => '1',
'aksrt_pay' => ['price' => '10.00'],
'jio_pay' => ['price' => '10.00'],
], $this->settings(false, false));
self::assertTrue($result['success']);
$meta = $GLOBALS['aksrt_test_post_meta'][104][DomainModel::PAY_META_KEY];
$meta = $GLOBALS['jio_test_post_meta'][104][DomainModel::PAY_META_KEY];
self::assertSame('0.00', $meta['vip1_price']);
self::assertSame('0.00', $meta['vip2_price']);
}
@@ -110,7 +110,7 @@ final class FrontSubmitPayTest extends TestCase {
$url = 'https://localhost/wp-content/uploads/2026/08/resource.zip';
$result = $this->submit->save(105, [
'pay_enabled' => '1',
'aksrt_pay' => [
'jio_pay' => [
'price' => '10.00',
'downloads' => [
[
@@ -122,7 +122,7 @@ final class FrontSubmitPayTest extends TestCase {
], $this->settings(false, false));
self::assertTrue($result['success']);
$meta = $GLOBALS['aksrt_test_post_meta'][105][DomainModel::PAY_META_KEY];
$meta = $GLOBALS['jio_test_post_meta'][105][DomainModel::PAY_META_KEY];
self::assertSame(DomainModel::PAY_TYPE_DOWNLOAD, $meta['pay_type']);
self::assertCount(1, $meta['downloads']);
self::assertSame($url, $meta['downloads'][0]['url']);
+8 -8
View File
@@ -1,7 +1,7 @@
<?php
use AKSRT\Ledger\AssetType;
use AKSRT\Ledger\LedgerCommand;
use Jio\Ledger\AssetType;
use Jio\Ledger\LedgerCommand;
use PHPUnit\Framework\TestCase;
final class LedgerCommandTest extends TestCase {
@@ -43,7 +43,7 @@ final class LedgerCommandTest extends TestCase {
]]);
$error = $command->validate();
self::assertInstanceOf(WP_Error::class, $error);
self::assertSame('aksrt_ledger_operation_key', $error->get_error_code());
self::assertSame('jio_ledger_operation_key', $error->get_error_code());
self::assertSame(400, $error->get_error_data()['status']);
}
@@ -72,7 +72,7 @@ final class LedgerCommandTest extends TestCase {
public function test_validation_rejects_empty_entries_and_invalid_asset(): void {
$empty = new LedgerCommand('empty:key', 'credit', []);
self::assertSame('aksrt_ledger_entries', $empty->validate()->get_error_code());
self::assertSame('jio_ledger_entries', $empty->validate()->get_error_code());
$invalid = new LedgerCommand('asset:key', 'credit', [[
'user_id' => 1,
@@ -80,7 +80,7 @@ final class LedgerCommandTest extends TestCase {
'delta_minor' => 1,
'entry_type' => 'credit',
]]);
self::assertSame('aksrt_ledger_asset', $invalid->validate()->get_error_code());
self::assertSame('jio_ledger_asset', $invalid->validate()->get_error_code());
}
public function test_validation_requires_integer_non_zero_entry_values(): void {
@@ -91,7 +91,7 @@ final class LedgerCommandTest extends TestCase {
'delta_minor' => $delta,
'entry_type' => 'credit',
]]);
self::assertSame('aksrt_ledger_entry', $command->validate()->get_error_code());
self::assertSame('jio_ledger_entry', $command->validate()->get_error_code());
}
}
@@ -102,7 +102,7 @@ final class LedgerCommandTest extends TestCase {
'delta_minor' => 1,
'entry_type' => 'credit',
]], '', '', -1);
self::assertSame('aksrt_ledger_actor', $actor->validate()->get_error_code());
self::assertSame('jio_ledger_actor', $actor->validate()->get_error_code());
$type = new LedgerCommand('type:key', 'credit', [[
'user_id' => 1,
@@ -110,6 +110,6 @@ final class LedgerCommandTest extends TestCase {
'delta_minor' => 1,
'entry_type' => 'Bad Type',
]]);
self::assertSame('aksrt_ledger_entry', $type->validate()->get_error_code());
self::assertSame('jio_ledger_entry', $type->validate()->get_error_code());
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
<?php
use AKSRT\Ledger\AssetType;
use AKSRT\Ledger\LedgerResult;
use Jio\Ledger\AssetType;
use Jio\Ledger\LedgerResult;
use PHPUnit\Framework\TestCase;
final class LedgerResultTest extends TestCase {
+2 -2
View File
@@ -1,13 +1,13 @@
<?php
use AKSRT\Ledger\OperationKey;
use Jio\Ledger\OperationKey;
use PHPUnit\Framework\TestCase;
final class OperationKeyTest extends TestCase {
public function test_request_key_requires_uuid_header(): void {
$error = OperationKey::from_request(new WP_REST_Request(), 'transfer', 9);
self::assertInstanceOf(WP_Error::class, $error);
self::assertSame('aksrt_idempotency_key', $error->get_error_code());
self::assertSame('jio_idempotency_key', $error->get_error_code());
self::assertSame(400, $error->get_error_data()['status']);
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
use AKSRT\Payment\Withdrawal;
use Jio\Payment\Withdrawal;
use PHPUnit\Framework\TestCase;
final class WithdrawalTest extends TestCase {
+6 -6
View File
@@ -28,7 +28,7 @@ function get_current_user_id() { return 1; }
function is_user_logged_in() { return true; }
function wp_unslash($value) { return $value; }
function current_time($type) { return date('Y-m-d H:i:s'); }
function get_theme_file_uri($file = '') { return 'https://example.test/wp-content/themes/aksrt/' . ltrim((string) $file, '/'); }
function get_theme_file_uri($file = '') { return 'https://example.test/wp-content/themes/jio/' . ltrim((string) $file, '/'); }
function get_theme_file_path($file = '') { return dirname(__DIR__, 2) . '/' . ltrim((string) $file, '/'); }
function get_template_directory() { return dirname(__DIR__, 2); }
function esc_url_raw($url) { return (string) $url; }
@@ -51,8 +51,8 @@ require $repo . '/src/Admin/SchemaProvider.php';
require $repo . '/src/Settings/Settings.php';
require $repo . '/src/Settings/UserRuntime.php';
$reasons = \AKSRT\Settings\UserRuntime::ban_settings()['reasons'] ?? null;
$report = \AKSRT\Settings\UserRuntime::report_settings();
$reasons = \Jio\Settings\UserRuntime::ban_settings()['reasons'] ?? null;
$report = \Jio\Settings\UserRuntime::report_settings();
echo "=== ban_settings()['reasons'] (empty option) ===\n";
var_export($reasons);
@@ -64,14 +64,14 @@ var_export([
]);
// 再模拟“后台保存过自定义原因”的场景
$GLOBALS['__test_options']['aksrt_settings'] = [
$GLOBALS['__test_options']['jio_settings'] = [
'ban_preset_reason' => [
['t' => '自定义原因A'],
['t' => '自定义原因B'],
],
];
\AKSRT\Settings\Settings::flush_settings_cache();
$stored = \AKSRT\Settings\UserRuntime::ban_settings()['reasons'] ?? null;
\Jio\Settings\Settings::flush_settings_cache();
$stored = \Jio\Settings\UserRuntime::ban_settings()['reasons'] ?? null;
echo "\n\n=== ban_settings()['reasons'] (custom stored) ===\n";
var_export($stored);
echo "\n";
+6 -6
View File
@@ -1,5 +1,5 @@
{
"schema": "aksrt.page-visual-matrix.v1",
"schema": "jio.page-visual-matrix.v1",
"reference": "https://localhost:9443",
"candidate": "https://localhost",
"viewports": {
@@ -12,11 +12,11 @@
"routes": [
{"id": "home", "path": "/", "states": ["default", "hover", "loading", "error"]},
{"id": "archive", "path": "/?post_type=post", "states": ["default", "numbered", "load-more", "infinite"]},
{"id": "category", "path": "/category/aksrt_zibll_v1_101/", "states": ["default", "empty"]},
{"id": "tag", "path": "/tag/aksrt_zibll_v1_103/", "states": ["default", "empty"]},
{"id": "search", "path": "/?s=AKSRT", "states": ["default", "empty", "focus"]},
{"id": "single", "path": "/aksrt_zibll_v1_301/", "states": ["default", "hover", "toc-open"]},
{"id": "resource", "path": "/aksrt_zibll_v1_303/", "states": ["locked", "authorized", "download"]},
{"id": "category", "path": "/category/jio_zibll_v1_101/", "states": ["default", "empty"]},
{"id": "tag", "path": "/tag/jio_zibll_v1_103/", "states": ["default", "empty"]},
{"id": "search", "path": "/?s=Jio", "states": ["default", "empty", "focus"]},
{"id": "single", "path": "/jio_zibll_v1_301/", "states": ["default", "hover", "toc-open"]},
{"id": "resource", "path": "/jio_zibll_v1_303/", "states": ["locked", "authorized", "download"]},
{"id": "page", "path": "/sample-page/", "states": ["default"]},
{"id": "login", "path": "/wp-login.php", "states": ["default", "focus", "error"]},
{"id": "user", "path": "/user/", "states": ["guest", "member", "vip"]},
+515 -515
View File
File diff suppressed because it is too large. Load diff
@@ -28,7 +28,7 @@ test('the Zibll toolbar affix rules stay removed', () => {
.map((entry) => read(`assets/js/${entry.name}`))
.join('\n');
// No AKSRT script adds .affix to .mce-top-part (that was Zibll's
// No Jio script adds .affix to .mce-top-part (that was Zibll's
// editextend.js), so the affix positioning rules must not come back.
assert.doesNotMatch(css, /mce-top-part\.affix/);
assert.doesNotMatch(js, /mce-top-part/);
@@ -15,7 +15,7 @@ test('featured box shows only the icon image/video buttons and no slide button',
// multi-select, so it renders whenever either capability is granted.
assert.match(
featured,
/if \(\$can_image \|\| \$can_slide\) : \?>\s*\n\s*<button type="button" class="but btn-image" data-featured-action="image"\s*\n\s*title="<\?php esc_attr_e\('图片', 'aksrt'\); \?>"\s*\n\s*aria-label="<\?php esc_attr_e\('图片', 'aksrt'\); \?>">\s*\n\s*<i class="fa fas fa-image"><\/i>\s*\n\s*<\/button>/
/if \(\$can_image \|\| \$can_slide\) : \?>\s*\n\s*<button type="button" class="but btn-image" data-featured-action="image"\s*\n\s*title="<\?php esc_attr_e\('图片', 'jio'\); \?>"\s*\n\s*aria-label="<\?php esc_attr_e\('图片', 'jio'\); \?>">\s*\n\s*<i class="fa fas fa-image"><\/i>\s*\n\s*<\/button>/
);
// The dedicated slide button is gone; slide covers come from multi-select.
@@ -17,11 +17,11 @@ test('featured box renders a playable video preview and a video-cover button', (
assert.match(featured, /fa fas fa-play-circle/);
// The video-cover button is capability-gated like the video button.
assert.match(featured, /if \(\$can_video\) : \?>\s*\n\s*<button type="button" class="but btn-video-pic" data-featured-action="video_pic"\s*\n\s*title="<\?php esc_attr_e\('设置视频封面', 'aksrt'\); \?>"/);
assert.match(featured, /if \(\$can_video\) : \?>\s*\n\s*<button type="button" class="but btn-video-pic" data-featured-action="video_pic"\s*\n\s*title="<\?php esc_attr_e\('设置视频封面', 'jio'\); \?>"/);
// The video row (video cover / video / clear) is icon-only with hover titles.
assert.match(featured, /btn-video" data-featured-action="video"\s*\n\s*title="<\?php esc_attr_e\('视频', 'aksrt'\); \?>"\s*\n\s*aria-label="<\?php esc_attr_e\('视频', 'aksrt'\); \?>">\s*\n\s*<i class="fa fas fa-video"><\/i>\s*\n\s*<\/button>/);
assert.match(featured, /btn-delete" data-featured-action="close"[\s\S]*?title="<\?php esc_attr_e\('清除', 'aksrt'\); \?>"/);
assert.match(featured, /btn-video" data-featured-action="video"\s*\n\s*title="<\?php esc_attr_e\('视频', 'jio'\); \?>"\s*\n\s*aria-label="<\?php esc_attr_e\('视频', 'jio'\); \?>">\s*\n\s*<i class="fa fas fa-video"><\/i>\s*\n\s*<\/button>/);
assert.match(featured, /btn-delete" data-featured-action="close"[\s\S]*?title="<\?php esc_attr_e\('清除', 'jio'\); \?>"/);
assert.doesNotMatch(featured, /首图|>视频<|>清除</);
// Dead capability attributes are gone.
@@ -78,7 +78,7 @@ test('i18n exposes the video-cover strings and dead keys are removed', () => {
const editorJs = read('assets/js/front-submit-editor.js');
const submitJs = read('assets/js/front-submit.js');
assert.match(page, /'setVideoCoverTitle' => __\('为视频设置封面', 'aksrt'\)/);
assert.match(page, /'setVideoCoverTitle' => __\('为视频设置封面', 'jio'\)/);
// The unused setVideoCover key died with the visible button label it served.
assert.doesNotMatch(page, /'setVideoCover' =>/);
assert.doesNotMatch(page, /首图/);
@@ -92,9 +92,9 @@ test('i18n exposes the video-cover strings and dead keys are removed', () => {
assert.doesNotMatch(submitJs, /i18n\.pickVideoCover/);
});
test('the unused aksrt_update_post ajax endpoint is removed', () => {
test('the unused jio_update_post ajax endpoint is removed', () => {
const frontSubmit = read('src/Core/FrontSubmit.php');
assert.doesNotMatch(frontSubmit, /aksrt_update_post/);
assert.doesNotMatch(frontSubmit, /jio_update_post/);
assert.doesNotMatch(frontSubmit, /ajax_update_post/);
});
@@ -12,7 +12,7 @@ const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
function renderHeaderParts() {
const php = String.raw`
define('ABSPATH', __DIR__ . '/');
$GLOBALS['aksrt_test_user_id'] = 0;
$GLOBALS['jio_test_user_id'] = 0;
function esc_attr($value) { return htmlspecialchars((string) $value, ENT_QUOTES); }
function esc_html($value) { return htmlspecialchars((string) $value, ENT_QUOTES); }
function esc_attr__($value, $domain = null) { return esc_attr($value); }
@@ -20,23 +20,23 @@ function esc_html__($value, $domain = null) { return esc_html($value); }
function esc_url($value) { return $value; }
function esc_url_raw($value) { return $value; }
function home_url($path = '/') { return 'https://example.test' . $path; }
function get_bloginfo($field = '') { return 'AKSRT'; }
function get_current_user_id() { return (int) $GLOBALS['aksrt_test_user_id']; }
function get_bloginfo($field = '') { return 'Jio'; }
function get_current_user_id() { return (int) $GLOBALS['jio_test_user_id']; }
function get_option($name, $default = false) { return $name === 'users_can_register'; }
function aksrt_render_header_menu_buttons() { return '<a class="newadd-btns">发布</a>'; }
function aksrt_get_translate_language_btn($class = '') { return '<a class="' . esc_attr($class) . '">语言</a>'; }
function aksrt_header_user_box_html($ctx) { return '<div class="sub-user-box">账户</div>'; }
function aksrt_get_avatar_html($userId, $size, $args = []) { return '<img class="avatar" width="' . (int) $size . '">'; }
function jio_render_header_menu_buttons() { return '<a class="newadd-btns">发布</a>'; }
function jio_get_translate_language_btn($class = '') { return '<a class="' . esc_attr($class) . '">语言</a>'; }
function jio_header_user_box_html($ctx) { return '<div class="sub-user-box">账户</div>'; }
function jio_get_avatar_html($userId, $size, $args = []) { return '<img class="avatar" width="' . (int) $size . '">'; }
function has_nav_menu($location) { return false; }
function is_super_admin() { return false; }
function is_singular($type = '') { return false; }
require __DIR__ . '/inc/helpers/header/aksrt-header-navbar.php';
require __DIR__ . '/inc/helpers/header/aksrt-header-mobile.php';
require __DIR__ . '/inc/helpers/header/jio-header-navbar.php';
require __DIR__ . '/inc/helpers/header/jio-header-mobile.php';
$results = [];
foreach (['1', '2', '3'] as $layout) {
foreach ([0, 7] as $userId) {
$GLOBALS['aksrt_test_user_id'] = $userId;
$GLOBALS['jio_test_user_id'] = $userId;
$ctx = [
'layout' => $layout,
'user_id' => $userId,
@@ -45,28 +45,28 @@ foreach (['1', '2', '3'] as $layout) {
'messages_url' => '',
'unread_count' => 0,
'mobile_layout' => 'center',
'brand_name' => 'AKSRT',
'brand_name' => 'Jio',
'logo_html' => '',
];
ob_start(); aksrt_menu_button($ctx); $buttons = ob_get_clean();
ob_start(); jio_menu_button($ctx); $buttons = ob_get_clean();
$results['desktop'][$layout][$userId] = $buttons;
}
$ctx['message_icon_positions'] = ['nav_menu'];
$ctx['messages_url'] = 'https://example.test/messages';
$ctx['unread_count'] = 3;
ob_start(); aksrt_menu_button($ctx); $results['notifications'][$layout] = ob_get_clean();
ob_start(); jio_menu_button($ctx); $results['notifications'][$layout] = ob_get_clean();
}
foreach (['center', 'left'] as $mobileLayout) {
$ctx = [
'mobile_layout' => $mobileLayout,
'mobile_align' => 'left',
'theme_positions' => [],
'brand_name' => 'AKSRT',
'brand_name' => 'Jio',
'logo_html' => '',
'user_id' => 0,
];
ob_start(); aksrt_navbar_header($ctx); $results['mobile'][$mobileLayout]['header'] = ob_get_clean();
ob_start(); aksrt_nav_mobile_content($ctx); $results['mobile'][$mobileLayout]['drawer'] = ob_get_clean();
ob_start(); jio_navbar_header($ctx); $results['mobile'][$mobileLayout]['header'] = ob_get_clean();
ob_start(); jio_nav_mobile_content($ctx); $results['mobile'][$mobileLayout]['drawer'] = ob_get_clean();
}
echo json_encode($results, JSON_UNESCAPED_SLASHES);
`;
@@ -82,9 +82,9 @@ test('rendered desktop layouts retain reference control order for guests and mem
const html = output.desktop[layout][userId];
assert.match(html, /class="but" aria-label=/, `layout ${layout} menu fold`);
assert.match(html, /class="list-inline splitters relative"/, `layout ${layout} account list`);
assert.doesNotMatch(html, /aksrt-navbar-account|aksrt-nav-avatar/);
assert.doesNotMatch(html, /jio-navbar-account|jio-nav-avatar/);
if (layout === '2' && userId === '0') assert.match(html, /class="signin-loader"/);
else assert.match(html, /class="aksrt-nav-icon[^"]*"|class="navbar-avatar"/);
else assert.match(html, /class="jio-nav-icon[^"]*"|class="navbar-avatar"/);
const right = html.indexOf('list-inline splitters relative');
const actions = html.indexOf('navbar-but');
if (layout === '3' && actions !== -1) assert.ok(right < actions, 'layout 3 puts account group before actions');
@@ -120,14 +120,14 @@ test('account icons keep the reference spacing on both sides of each divider', (
assert.doesNotMatch(account[1], /<\/li>\s+<li\b/, 'extra text space shifts the divider');
assert.doesNotMatch(account[1], /<\/li>\s+$/, 'trailing text space widens the action group');
}
assert.match(output.notifications['1'], /aksrt-nav-notification/);
assert.match(output.notifications['1'], /jio-nav-notification/);
});
test('header source restores the reference typography and Bootstrap control dimensions', () => {
const css = read('assets/css/frontend/header-userbox.css');
const mainCss = read('assets/css/main.css');
assert.match(css, /\.header,\s*\.mobile-header,\s*\.navbar-search\s*\{[^}]*font-family:\s*"Helvetica Neue", Helvetica, Arial, sans-serif;[^}]*font-size:\s*14px;[^}]*font-weight:\s*400;[^}]*line-height:\s*1\.42857143;[^}]*-webkit-font-smoothing:\s*auto;[^}]*text-rendering:\s*auto;/s);
assert.match(css, /\.navbar-top \.aksrt-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s);
assert.match(css, /\.navbar-top \.jio-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s);
assert.match(mainCss, /\.navbar-avatar\s*\{[^}]*margin-top:\s*3px;[^}]*width:\s*29px;[^}]*height:\s*29px;/s);
assert.doesNotMatch(css, /\.aksrt-navbar-account|\.aksrt-nav-avatar|\.navbar-form \.toggle-radius:not|navbar-form\.flex\.ac\s*\{[^}]*min-height|\.navbar-search(?:\.show)?\s*\{[^}]*transform:/s);
assert.doesNotMatch(css, /\.jio-navbar-account|\.jio-nav-avatar|\.navbar-form \.toggle-radius:not|navbar-form\.flex\.ac\s*\{[^}]*min-height|\.navbar-search(?:\.show)?\s*\{[^}]*transform:/s);
});
@@ -28,20 +28,20 @@ const scripts = join([
'src/Core/ScriptsPart1b.php',
'src/Core/ScriptsPart1c.php',
]);
// The Font Awesome handle is registered inside aksrt_enqueue_fontawesome(), so the
// The Font Awesome handle is registered inside jio_enqueue_fontawesome(), so the
// 5.15.4 asset contract has to follow that indirection rather than the enqueue call site.
const iconRuntime = join([
'src/Core/ScriptsPart1b.php',
'src/Core/ScriptsPart1c.php',
'inc/helpers/aksrt-icons.php',
'inc/helpers/jio-icons.php',
]);
const header = join([
'header.php',
'inc/helpers/header/aksrt-header-render.php',
'inc/helpers/header/aksrt-header-navbar.php',
'inc/helpers/header/aksrt-header-mobile.php',
'inc/helpers/aksrt-header-menu-buttons.php',
'inc/helpers/aksrt-header-userbox.php',
'inc/helpers/header/jio-header-render.php',
'inc/helpers/header/jio-header-navbar.php',
'inc/helpers/header/jio-header-mobile.php',
'inc/helpers/jio-header-menu-buttons.php',
'inc/helpers/jio-header-userbox.php',
]);
test('the public dependency graph uses WordPress jQuery with the local runtime stack', () => {
@@ -49,7 +49,7 @@ test('the public dependency graph uses WordPress jQuery with the local runtime s
assert.doesNotMatch(scripts, /wp_deregister_script\('jquery'\)/);
assert.doesNotMatch(scripts, /jquery\.min\.js'[\s\S]*?'1\.9\.1'/);
assert.match(scripts, /bootstrap\.min\.css'[\s\S]*?'3\.4\.1'/);
assert.match(scripts, /aksrt_enqueue_fontawesome\(/);
assert.match(scripts, /jio_enqueue_fontawesome\(/);
assert.match(iconRuntime, /fontawesome5\/css\/all\.min\.css'[\s\S]*?'5\.15\.4'/);
assert.match(scripts, /main\.min\.css/);
assert.match(scripts, /loader\.js'[\s\S]*?\['jquery', 'bootstrap'\]/);
@@ -63,7 +63,7 @@ test('the public dependency graph uses WordPress jQuery with the local runtime s
'assets/js/runtime/main.min.js',
]) assert.equal(exists(file), true, `${file} must be local`);
assert.doesNotMatch(scripts, /bootstrap5|bootstrap-5|font-awesome\.min\.css|swiper(?:\.min)?\.(?:css|js)|aksrt-mini-touch/);
assert.doesNotMatch(scripts, /bootstrap5|bootstrap-5|font-awesome\.min\.css|swiper(?:\.min)?\.(?:css|js)|jio-mini-touch/);
assert.equal(exists('assets/css/frontend/nav.css'), false);
assert.equal(exists('assets/js/core/nav.js'), false);
assert.equal(exists('assets/js/core/search.js'), false);
@@ -72,7 +72,7 @@ test('the public dependency graph uses WordPress jQuery with the local runtime s
assert.equal(exists('assets/js/mini-touch.js'), false);
});
test('payment entry points use only the AKSRT payment runtime', () => {
test('payment entry points use only the Jio payment runtime', () => {
const loader = read('assets/js/runtime/loader.js');
const main = join([
'assets/js/runtime/main.js',
@@ -88,9 +88,9 @@ test('payment entry points use only the AKSRT payment runtime', () => {
assert.doesNotMatch(main, /tbquire\(\[['"]pay['"]\]\)/);
assert.doesNotMatch(main, /indexOf\(['"]zippay['"]\)/);
assert.match(paymentRuntime, /\.pay-vip/);
assert.match(paymentRuntime, /data-aksrt-pay-content/);
assert.match(paymentRuntime, /'aksrt-pay-vip'/);
assert.match(paymentRuntime, /'aksrt-cms-download'/);
assert.match(paymentRuntime, /data-jio-pay-content/);
assert.match(paymentRuntime, /'jio-pay-vip'/);
assert.match(paymentRuntime, /'jio-cms-download'/);
});
test('Font Awesome 5 is the only icon contract across every runtime path', () => {
@@ -159,7 +159,7 @@ test('guest account member VIP and publish contracts share reference header DOM'
const userboxCss = read('assets/css/frontend/header-userbox.css');
assert.match(header, /is_user_logged_in\(\)/);
assert.match(header, /sub-user-box aksrt-header-userbox/);
assert.match(header, /sub-user-box jio-header-userbox/);
assert.match(header, /signin-loader|signin/);
assert.match(header, /get_payment_vip_settings/);
assert.match(header, /nav_user_newposts_btn/);
@@ -173,12 +173,12 @@ test('guest account member VIP and publish contracts share reference header DOM'
assert.doesNotMatch(userboxCss, /--(?:main|muted|key)-|@import/);
// Account rows render through the shared legacy classes; the module keeps only
// the shell anchoring, the account href geometry and the top-nav bell badge.
assert.match(userboxCss, /\.aksrt-nav-notification \.aksrt-notification-badge/);
assert.doesNotMatch(userboxCss, /aksrt-header-wallet|aksrt-header-checkin|aksrt-header-payvip|\.aksrt-header-userbox \.aksrt-notification-badge/);
assert.match(userboxCss, /\.jio-nav-notification \.jio-notification-badge/);
assert.doesNotMatch(userboxCss, /jio-header-wallet|jio-header-checkin|jio-header-payvip|\.jio-header-userbox \.jio-notification-badge/);
assert.doesNotMatch(userboxCss, /width:\s*260px/);
assert.match(userboxCss, /\.aksrt-header-userbox\s*\{[^}]*line-height:\s*1\.42857143/s);
assert.match(userboxCss, /\.aksrt-header-userbox\s+\.muted-box\.padding-6\s*\{[^}]*background-color:\s*var\(--this-bg,\s*var\(--color-line-soft\)\)[^}]*border-radius:\s*4px[^}]*padding:\s*6px/s);
assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s);
assert.match(userboxCss, /\.jio-header-userbox\s*\{[^}]*line-height:\s*1\.42857143/s);
assert.match(userboxCss, /\.jio-header-userbox\s+\.muted-box\.padding-6\s*\{[^}]*background-color:\s*var\(--this-bg,\s*var\(--color-line-soft\)\)[^}]*border-radius:\s*4px[^}]*padding:\s*6px/s);
assert.match(userboxCss, /\.jio-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s);
});
test('the top navigation keeps the reference Bootstrap control geometry', () => {
@@ -186,8 +186,8 @@ test('the top navigation keeps the reference Bootstrap control geometry', () =>
const publicCss = read('assets/css/build/public.css');
assert.match(userboxCss, /\.header,\s*\.mobile-header,\s*\.navbar-search\s*\{[^}]*font-family:\s*['"]Helvetica Neue['"],\s*Helvetica,\s*Arial,\s*sans-serif;[^}]*font-weight:\s*400;[^}]*line-height:\s*1\.42857143;/s);
assert.match(userboxCss, /\.navbar-top \.aksrt-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s);
assert.doesNotMatch(userboxCss, /\.aksrt-navbar-account|\.aksrt-nav-avatar|\.navbar-form \.toggle-radius:not|navbar-form\.flex\.ac\s*\{[^}]*min-height|\.aksrt-navbar-language\s*\{[^}]*margin:/);
assert.match(userboxCss, /\.navbar-top \.jio-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s);
assert.doesNotMatch(userboxCss, /\.jio-navbar-account|\.jio-nav-avatar|\.navbar-form \.toggle-radius:not|navbar-form\.flex\.ac\s*\{[^}]*min-height|\.jio-navbar-language\s*\{[^}]*margin:/);
assert.doesNotMatch(publicCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\)/);
});
@@ -221,23 +221,23 @@ test('route and feature modules do not restyle the legacy top-nav chips', () =>
})
test('the account popover rows match the reference user box structure', () => {
const identity = read('inc/helpers/aksrt-identity.php');
const identity = read('inc/helpers/jio-identity.php');
const legacyMainCss = read('assets/css/main.css');
const buttonsCss = read('assets/css/core/buttons.css');
const userboxCss = read('assets/css/frontend/header-userbox.css');
// Stats row is wired with the reference wrapper and carries the posts chip.
assert.match(header, /aksrt_header_user_badges_row\(\$user_id\)/);
assert.match(header, /jio_header_user_badges_row\(\$user_id\)/);
assert.match(header, /class="em09 author-tag mb10 mt6 flex jc"/);
assert.match(identity, /tag-posts/);
assert.match(identity, /fa fas fa-file-alt/);
assert.match(identity, /fa fas fa-comment-dots/);
assert.match(identity, /fa fas fa-thumbs-up/);
assert.match(identity, /fa fas fa-fire/);
assert.doesNotMatch(identity, /aksrt_renderer_svg_icon/);
assert.doesNotMatch(identity, /jio_renderer_svg_icon/);
// Admin row exposes the AKSRT payment center at the reference mall slot.
assert.match(header, /admin\.php\?page=aksrt_pay/);
// Admin row exposes the Jio payment center at the reference mall slot.
assert.match(header, /admin\.php\?page=jio_pay/);
assert.match(header, /支付中心/);
// Wallet tiles: balance first, muted-box geometry, reference sprite icons.
@@ -245,8 +245,8 @@ test('the account popover rows match the reference user box structure', () => {
assert.match(header, /muted-box padding-6 flex1 radius4/);
assert.match(header, /font-bold c-blue-2/);
assert.match(header, /font-bold c-yellow/);
assert.match(header, /aksrt_icon_markup\('zibsvg-money-color-2', 'em14'\)/);
assert.match(header, /aksrt_icon_markup\('zibsvg-points-color', 'em14'\)/);
assert.match(header, /jio_icon_markup\('zibsvg-money-color-2', 'em14'\)/);
assert.match(header, /jio_icon_markup\('zibsvg-points-color', 'em14'\)/);
// The value icons are the reference theme's multi-tone SVGs, not FA5 glyphs;
// the tile arrow stays on fa-angle-right exactly as the reference renders it.
assert.doesNotMatch(header, /fa fas fa-money-bill-wave/);
@@ -267,18 +267,18 @@ test('the account popover rows match the reference user box structure', () => {
// cover background, color, border and text-shadow, and must not be scoped to
// one call site or the top-nav chip keeps the wrong gradient.
assert.match(userboxCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;[\s\S]*?border:\s*none;[\s\S]*?text-shadow:\s*none;/s);
assert.doesNotMatch(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon/)
assert.doesNotMatch(userboxCss, /\.jio-header-userbox \.pay-vip\.but\.payvip-icon/)
assert.doesNotMatch(header, /vipbg-v2/);
// Check-in entry is an anchor with the reference button classes.
assert.match(header, /but block c-yellow/);
assert.match(header, /aksrt_get_user_checkin_details_link\([\s\S]*?'but block c-yellow'/);
assert.match(header, /jio_get_user_checkin_details_link\([\s\S]*?'but block c-yellow'/);
assert.match(header, /checkin-details-link/);
assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/);
assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/);
// The hover wrapper uses the reference inline-block utility so the absolutely
// positioned VIP card keeps its original static position.
assert.match(read('inc/helpers/aksrt-header-menu-buttons.php'), /class="hover-show inline-block"/);
assert.match(read('inc/helpers/jio-header-menu-buttons.php'), /class="hover-show inline-block"/);
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s);
assert.doesNotMatch(read('assets/css/frontend/header-userbox.css'), /\.navbar-top \.navbar-but > \.hover-show/);
assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/);
@@ -303,7 +303,7 @@ test('the only menu metadata and Walker contract is the legacy runtime', () => {
const decoration = read('inc/helpers/menu-decoration.php');
const source = walker + menuAdmin + decoration;
assert.match(source, /aksrt_menu_options/);
assert.match(source, /jio_menu_options/);
assert.doesNotMatch(source, /zib_menu_options/);
assert.match(walker, /start_lvl|start_el|end_el/);
assert.match(walker, /submenu_graphic_card/);
@@ -317,13 +317,13 @@ test('the only menu metadata and Walker contract is the legacy runtime', () => {
});
test('search is footer-lazy and exposes only reference actions and request fields', () => {
const shell = read('inc/helpers/header/aksrt-header-menu-search.php');
const box = read('inc/helpers/search/aksrt-search-box.php');
const shell = read('inc/helpers/header/jio-header-menu-search.php');
const box = read('inc/helpers/search/jio-search-box.php');
const results = read('search.php');
const settings = read('src/Settings/ThemeSearch.php');
const source = shell + box + results + settings;
assert.match(shell, /add_action\('wp_footer', 'aksrt_footer_navbar_search'/);
assert.match(shell, /add_action\('wp_footer', 'jio_footer_navbar_search'/);
assert.match(shell, /wp_ajax_menu_search/);
assert.match(shell, /wp_ajax_search_box/);
assert.match(shell, /wp_verify_nonce/);
@@ -335,9 +335,9 @@ test('search is footer-lazy and exposes only reference actions and request field
assert.match(box, /array_slice\(\$history, 0, 30\)/);
assert.match(box, /search_keywords/);
assert.match(settings, /search_custom_key/);
assert.match(settings, /aksrt_search_terms\.name LIKE/);
assert.match(settings, /jio_search_terms\.name LIKE/);
assert.match(results, /search-tab-filter/);
assert.match(results, /aksrt_get_search_tab_nav_filter/);
assert.match(results, /jio_get_search_tab_nav_filter/);
assert.equal(exists('templates/parts/navbar-search.php'), false);
assert.equal(exists('templates/parts/search-panel.php'), false);
});
@@ -366,8 +366,8 @@ test('header slider is the only top visual path and uses global plus page_config
const defaults = read('src/Settings/DisplayPageGroups.php');
const source = renderer + globalSchema + pageMeta + defaults;
assert.match(renderer, /function aksrt_header_slide\(\): void/);
assert.match(renderer, /function aksrt_header_slide_is_show\(\): bool/);
assert.match(renderer, /function jio_header_slide\(\): void/);
assert.match(renderer, /function jio_header_slide_is_show\(\): bool/);
assert.match(pageMeta, /page_config/);
for (const key of [
'header_slider_show', 'header_slider_show_type', 'header_slider_nav_color',
@@ -404,6 +404,6 @@ test('old navigation settings and runtime handles do not survive the hard switch
'src/Core/ScriptsPart1c.php',
'assets/js/runtime/main.js',
]);
assert.doesNotMatch(source, /bootstrap5|aksrt-mini-touch|aksrt\.Drawer|aksrt\.minitouch|updateScrollBus/);
assert.doesNotMatch(source, /aksrt_nav_|legacy_nav|nav_compat/);
assert.doesNotMatch(source, /bootstrap5|jio-mini-touch|jio\.Drawer|jio\.minitouch|updateScrollBus/);
assert.doesNotMatch(source, /jio_nav_|legacy_nav|nav_compat/);
});
+1 -1
View File
@@ -2,7 +2,7 @@
This directory is intentionally empty until validation runs against the user-provided WordPress site.
The browser run must create the screenshot paths declared by `tests/fixtures/widget-golden-v1.json` and a `manifest.json` using schema `aksrt.widget-visual-results.v1`. Validate completed results with:
The browser run must create the screenshot paths declared by `tests/fixtures/widget-golden-v1.json` and a `manifest.json` using schema `jio.widget-visual-results.v1`. Validate completed results with:
```text
php tools/validate-widget-golden.php --require-results
+1 -1
View File
@@ -1,4 +1,4 @@
# AKSRT widget static accessibility audit
# Jio widget static accessibility audit
Scope: `src/WidgetSystem/Renderers/`, `assets/widgets/frontend.js`, and `assets/widgets/frontend.css`.
+5 -5
View File
@@ -1,17 +1,17 @@
# AKSRT / Zibll visual reference
# Jio / Zibll visual reference
The reference stack is isolated from the AKSRT site and exposes Zibll at
The reference stack is isolated from the Jio site and exposes Zibll at
https://localhost:9443. The local Zibll source remains read-only.
~~~powershell
$env:ZIBLL_THEME_PATH='C:\Users\Xing\Desktop\zibll'
docker compose -f tests/visual/docker-compose.zibll.yml up -d
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp core install --url=https://localhost:9443 --title='Zibll Reference' --admin_user=visual_admin --admin_password='AKSRT-Visual-2026!' --admin_email=visual@example.test --skip-email
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp core install --url=https://localhost:9443 --title='Zibll Reference' --admin_user=visual_admin --admin_password='Jio-Visual-2026!' --admin_email=visual@example.test --skip-email
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp theme activate zibll
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp eval-file /visual-fixtures/article-fixtures.php create
~~~
Run the same theme-neutral article generator inside the AKSRT WordPress
Run the same theme-neutral article generator inside the Jio WordPress
container. Mount `tests/visual/` read-only at `/visual-fixtures` in that stack:
~~~powershell
@@ -22,7 +22,7 @@ wp eval-file /visual-fixtures/article-fixtures.php cleanup
The shared generator creates the same authors, category, tag, dates, images,
featured/no-image/multi-image/first-image/external-cover posts, slide and video
metadata, pay/VIP states, sticky state, and interaction counters in both sites.
Fixture-only Zibll and AKSRT metadata are written together so the rendered
Fixture-only Zibll and Jio metadata are written together so the rendered
business state is identical without adding compatibility code to either theme.
Use `tests/visual/article-visual-matrix-v1.json` for the article-only captures.
+12 -12
View File
@@ -1,6 +1,6 @@
<?php
/**
* Theme-neutral article fixtures shared by the Zibll and AKSRT visual sites.
* Theme-neutral article fixtures shared by the Zibll and Jio visual sites.
*
* Usage: wp eval-file /visual-fixtures/article-fixtures.php create|cleanup
*/
@@ -76,7 +76,7 @@ $manifest = [
'files' => [],
'options' => [],
];
foreach (['aksrt_settings', 'zibll_options'] as $option_name) {
foreach (['jio_settings', 'zibll_options'] as $option_name) {
$manifest['options'][$option_name] = [
'exists' => get_option($option_name, null) !== null,
'value' => get_option($option_name),
@@ -110,7 +110,7 @@ try {
'post_related_orderby' => 'views',
'related_title' => '相关推荐',
];
foreach (['aksrt_settings', 'zibll_options'] as $option_name) {
foreach (['jio_settings', 'zibll_options'] as $option_name) {
$current = get_option($option_name, []);
update_option($option_name, array_merge(is_array($current) ? $current : [], $visual_options), false);
}
@@ -123,7 +123,7 @@ try {
foreach ($authors as $author) {
$user_id = wp_insert_user([
'user_login' => $author['login'],
'user_pass' => 'AKSRT-Visual-2026!',
'user_pass' => 'Jio-Visual-2026!',
'user_email' => $author['login'] . '@example.test',
'display_name' => $author['name'],
'description' => '固定作者简介,用于文章卡片视觉对比。',
@@ -197,7 +197,7 @@ try {
$term_other['cover_image'] = $image_urls[4];
update_term_meta($category_id, 'zib_other_data', $term_other);
update_term_meta($category_id, 'cover_image', $image_urls[4]);
update_term_meta($category_id, 'aksrt_category_cover_id', $attachment_ids[4]);
update_term_meta($category_id, 'jio_category_cover_id', $attachment_ids[4]);
$scenarios = [
['slug' => 'featured', 'title' => '特色图文章:标准列表与卡片基线', 'featured' => 0],
@@ -211,7 +211,7 @@ try {
['slug' => 'first-image', 'title' => '文章首图作为缩略图', 'first_image' => 3],
];
foreach ($scenarios as $index => $scenario) {
$content = '<p>固定摘要正文,用于 Zibll 与 AKSRT 的文章列表逐像素比较。</p>';
$content = '<p>固定摘要正文,用于 Zibll 与 Jio 的文章列表逐像素比较。</p>';
if (!empty($scenario['gallery'])) {
$content .= implode('', array_map(static fn(string $url): string => '<img src="' . esc_url($url) . '" alt="">', array_slice($image_urls, 0, 4)));
} elseif (isset($scenario['first_image'])) {
@@ -262,15 +262,15 @@ try {
$zib_meta['views'] = 1200 + ($index * 137);
$zib_meta['like'] = 30 + ($index * 7);
update_post_meta($post_id, 'zib_other_data', $zib_meta);
update_post_meta($post_id, 'aksrt_post_meta', $post_meta);
update_post_meta($post_id, 'aksrt_post_views', $zib_meta['views']);
update_post_meta($post_id, '_aksrt_like_count', $zib_meta['like']);
update_post_meta($post_id, '_aksrt_favorite_count', 10 + $index);
update_post_meta($post_id, 'jio_post_meta', $post_meta);
update_post_meta($post_id, 'jio_post_views', $zib_meta['views']);
update_post_meta($post_id, '_jio_like_count', $zib_meta['like']);
update_post_meta($post_id, '_jio_favorite_count', 10 + $index);
$pay_type = (string) ($scenario['pay'] ?? 'no');
$pay_limit = $pay_type === 'vip' ? 2 : 0;
$pay_price = $pay_type === 'reading' ? 19.9 : 0;
update_post_meta($post_id, '_aksrt_pay', [
update_post_meta($post_id, '_jio_pay', [
'pay_type' => $pay_type,
'pay_modo' => '0',
'price' => $pay_price,
@@ -287,7 +287,7 @@ try {
stick_post((int) $manifest['posts'][0]);
}
update_option($manifest_key, $manifest, false);
WP_CLI::success('Created identical Zibll/AKSRT article fixtures.');
WP_CLI::success('Created identical Zibll/Jio article fixtures.');
} catch (Throwable $error) {
$cleanup($manifest);
WP_CLI::error('Fixture creation rolled back: ' . $error->getMessage());
+1 -1
View File
@@ -1,5 +1,5 @@
{
"schema": "aksrt.zibll-article-visual-matrix.v1",
"schema": "jio.zibll-article-visual-matrix.v1",
"reference": "https://localhost:9443",
"candidate": "https://localhost",
"fixture": "/visual-fixtures/article-fixtures.php",
+1 -1
View File
@@ -1,4 +1,4 @@
name: aksrt-zibll-visual-reference
name: jio-zibll-visual-reference
services:
db:
+1 -1
View File
@@ -1,5 +1,5 @@
{
"schema": "aksrt.zibll-article-baseline.v1",
"schema": "jio.zibll-article-baseline.v1",
"referencePath": "C:\\Users\\Xing\\Desktop\\zibll",
"theme": "Zibll",
"version": "9.0",
@@ -17,44 +17,44 @@ test('account dialogs expose configured email steps, phone steps, and reference
assert.match(template, /\$email_verify_required = \$account_email !== '' && !empty\(\$user_settings\['email_set_captch'\]\)/);
assert.match(template, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"[\s\S]*?data-account-step="email-success"/);
assert.match(template, /if \(\$email_verify_required\)/);
assert.match(template, /if \(\$email_verify_required\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/);
assert.match(template, /if \(\$email_verify_required\)[\s\S]*?else : \?>[\s\S]*?jio-settings-modal__pane/);
assert.match(template, /if \(!empty\(\$user_settings\['email_set_captch'\]\)\)/);
assert.match(template, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"[\s\S]*?data-account-step="phone-success"/);
assert.match(template, /if \(\$account_phone !== ''\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/);
assert.match(template, /data-aksrt-sms-scene="verify_old_phone"/);
assert.match(template, /data-aksrt-sms-scene="bind_new_phone"/);
assert.match(template, /\$account_close_icon = function_exists\('aksrt_icon_markup'\)/);
assert.match(template, /if \(\$account_phone !== ''\)[\s\S]*?else : \?>[\s\S]*?jio-settings-modal__pane/);
assert.match(template, /data-jio-sms-scene="verify_old_phone"/);
assert.match(template, /data-jio-sms-scene="bind_new_phone"/);
assert.match(template, /\$account_close_icon = function_exists\('jio_icon_markup'\)/);
assert.match(template, /<i class="fa far fa-envelope"/);
assert.match(template, /\$account_phone_display = function_exists\('aksrt_phone_mask_display'\) \? aksrt_phone_mask_display\(\$account_phone\) : \$account_phone;/);
assert.match(template, /\$account_phone_display = function_exists\('jio_phone_mask_display'\) \? jio_phone_mask_display\(\$account_phone\) : \$account_phone;/);
assert.match(template, /\$account_phone !== '' \? \$account_phone_display : __\('暂未绑定'/);
assert.doesNotMatch(template, /为确认是您本人操作,请补全绑定手机号 %s/);
assert.match(template, /为确认是您本人操作,请补全绑定手机号/);
assert.match(template, /class="aksrt-phone-digit__edge"><\?php echo esc_html\(\(\$phone_mask\['leading_plus'\] \? '\+' : ''\) \. \(string\) \$phone_mask\['prefix'\]\); \?><\/span>/);
assert.match(template, /<span class="aksrt-phone-digit__edge"><\?php echo esc_html\(\(string\) \$phone_mask\['suffix'\]\); \?><\/span>/);
assert.match(template, /class="jio-phone-digit__edge"><\?php echo esc_html\(\(\$phone_mask\['leading_plus'\] \? '\+' : ''\) \. \(string\) \$phone_mask\['prefix'\]\); \?><\/span>/);
assert.match(template, /<span class="jio-phone-digit__edge"><\?php echo esc_html\(\(string\) \$phone_mask\['suffix'\]\); \?><\/span>/);
assert.doesNotMatch(template, /name="phone" value="/);
assert.match(template, /data-aksrt-phone-digits data-hidden-length/);
assert.match(template, /class="aksrt-phone-digit" inputmode="numeric" maxlength="1"/);
assert.match(template, /aria-label="<\?php echo esc_attr\(sprintf\(__\('手机号第 %d 位数字', 'aksrt'\), 4 \+ \$phone_digit_index\)\); \?>"/);
assert.match(template, /data-aksrt-phone-code-area<\?php echo \$phone_mask !== null \? ' hidden' : ''; \?>/);
assert.match(template, /data-aksrt-phone-digits-error/);
assert.match(template, /data-jio-phone-digits data-hidden-length/);
assert.match(template, /class="jio-phone-digit" inputmode="numeric" maxlength="1"/);
assert.match(template, /aria-label="<\?php echo esc_attr\(sprintf\(__\('手机号第 %d 位数字', 'jio'\), 4 \+ \$phone_digit_index\)\); \?>"/);
assert.match(template, /data-jio-phone-code-area<\?php echo \$phone_mask !== null \? ' hidden' : ''; \?>/);
assert.match(template, /data-jio-phone-digits-error/);
assert.match(javascript, /\/user\/phone-match/);
assert.match(javascript, /middle: middle/);
assert.match(javascript, /stage: 'reset_old_phone'/);
assert.match(javascript, /function initPhoneDigits\(/);
assert.match(css, /\.aksrt-settings-modal \.aksrt-phone-digit \{/);
assert.match(css, /\.aksrt-settings-modal \.aksrt-phone-digit__edge \{/);
assert.match(css, /\.jio-settings-modal \.jio-phone-digit \{/);
assert.match(css, /\.jio-settings-modal \.jio-phone-digit__edge \{/);
assert.match(css, /background-color: var\(--main-bg-color, #f7f8fa\)/);
assert.match(css, /\.aksrt-settings-modal \.aksrt-phone-digits\.has-error \.aksrt-phone-digit \{/);
assert.match(template, /aksrt_render_captcha\('change_email'\)[\s\S]*?id="email-code"/);
assert.match(template, /aksrt_render_captcha\('bind_phone'\)[\s\S]*?id="bind-phone-code"/);
assert.match(css, /\.jio-settings-modal \.jio-phone-digits\.has-error \.jio-phone-digit \{/);
assert.match(template, /jio_render_captcha\('change_email'\)[\s\S]*?id="email-code"/);
assert.match(template, /jio_render_captcha\('bind_phone'\)[\s\S]*?id="bind-phone-code"/);
assert.match(template, /if \(!\$account_oauth_new\)[\s\S]*?name="passwordold"/);
assert.match(template, /if \(!\$account_oauth_new && \$can_recover_password\)/);
assert.match(template, /data-account-password-pane="recovery"/);
assert.match(javascript, /\/auth\/code/);
assert.match(javascript, /\/auth\/reset-password/);
assert.match(javascript, /className: 'aksrt-user-settings-modal modal-mini full-sm'/);
assert.match(javascript, /className: 'jio-user-settings-modal modal-mini full-sm'/);
assert.doesNotMatch(template + javascript + css, /pwd-strength|pwd-send-code|email-send-code/);
assert.match(css, /\.aksrt-modal-runtime\.aksrt-user-settings-modal\.aksrt-modal-runtime--bottom \.aksrt-modal-runtime__dialog \{[\s\S]*?width: 400px/);
assert.match(css, /\.jio-modal-runtime\.jio-user-settings-modal\.jio-modal-runtime--bottom \.jio-modal-runtime__dialog \{[\s\S]*?width: 400px/);
assert.match(css, /@media \(max-width: 640px\)[\s\S]*?width: 100%/);
assert.match(css, /modal-line-field--code \.yztx\.abs-right[\s\S]*?top: 50%[\s\S]*?right: \.7em/);
assert.doesNotMatch(css, /\.code-send-btn \{ min-width:/);
@@ -83,25 +83,25 @@ test('account PHP harness exercises email and password state transitions', () =>
test('email update keeps both proofs through validation failures and consumes them only after the write', () => {
const ajax = read('inc/modules/user-parts/account-ajax.php');
const send = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_send_account_email_code'"), ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'"));
const verify = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_password'"));
const change = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_email'"));
const send = ajax.slice(ajax.indexOf("add_action('wp_ajax_jio_send_account_email_code'"), ajax.indexOf("add_action('wp_ajax_jio_verify_account_email'"));
const verify = ajax.slice(ajax.indexOf("add_action('wp_ajax_jio_verify_account_email'"), ajax.indexOf("add_action('wp_ajax_jio_change_password'"));
const change = ajax.slice(ajax.indexOf("add_action('wp_ajax_jio_change_email'"));
assert.match(send, /\['verify_email', 'change_email'\]/);
assert.match(send, /email_exists\(\$target\)/);
assert.match(send, /SendLimit::check\(\$target\)/);
assert.match(send, /SendLimit::record\(\$target\)/);
assert.doesNotMatch(send, /aksrt_account_email_rate_/);
assert.doesNotMatch(ajax, /aksrt_account_email_rate_/);
assert.match(verify, /aksrt_require_captcha\(\s*'change_email'/);
assert.match(verify, /aksrt_account_set_proof\(\$user_id, 'email_old'/);
assert.match(change, /aksrt_account_email_verification_required\(\)/);
assert.match(change, /aksrt_account_consume_proof\([^;]+false\)/);
assert.match(change, /aksrt_account_consume_code\([\s\S]*?false\s*\)/);
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_proof_key"));
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_code_key"));
assert.doesNotMatch(send, /jio_account_email_rate_/);
assert.doesNotMatch(ajax, /jio_account_email_rate_/);
assert.match(verify, /jio_require_captcha\(\s*'change_email'/);
assert.match(verify, /jio_account_set_proof\(\$user_id, 'email_old'/);
assert.match(change, /jio_account_email_verification_required\(\)/);
assert.match(change, /jio_account_consume_proof\([^;]+false\)/);
assert.match(change, /jio_account_consume_code\([\s\S]*?false\s*\)/);
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(jio_account_proof_key"));
assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(jio_account_code_key"));
assert.match(change, /\$agreement\['required'\]/);
assert.doesNotMatch(ajax, /wp_ajax_aksrt_send_email_code/);
assert.doesNotMatch(ajax, /wp_ajax_jio_send_email_code/);
});
test('phone replacement requires old-number proof and a new-number code while reusing bind side effects', () => {
@@ -123,29 +123,29 @@ test('phone replacement requires old-number proof and a new-number code while re
assert.match(code, /\$phone = \$current_phone;/);
assert.doesNotMatch(code, /auth_rate_limited\('account-phone-code'/);
assert.match(verify, /auth_consume_code\('account_verify_old_phone_' \./);
assert.match(verify, /aksrt_account_phone_old_/);
assert.match(bind, /aksrt_account_phone_old_/);
assert.match(verify, /jio_account_phone_old_/);
assert.match(bind, /jio_account_phone_old_/);
assert.match(bind, /auth_consume_code\(\$code_scene, \$phone, \$code, false\)/);
assert.ok(bind.indexOf("Auth\\Service::bind_phone") < bind.indexOf("delete_transient($this->auth_code_key($code_scene, $phone))"));
assert.match(bind, /aksrt_get_auth_agreement_view/);
assert.match(bind, /jio_get_auth_agreement_view/);
assert.match(bind, /Auth\\Service::bind_phone\(\$payload, true\)/);
assert.match(binding, /aksrt_user_phone_changed/);
assert.match(binding, /jio_user_phone_changed/);
assert.doesNotMatch(controller, /auth_consume_code\('bind_phone'/);
});
test('password change retains old-password limits and captcha; recovery uses shared auth endpoints', () => {
const ajax = read('inc/modules/user-parts/account-ajax.php');
const javascript = read('assets/js/routes/user-center.js');
const password = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_password'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_email'"));
const password = ajax.slice(ajax.indexOf("add_action('wp_ajax_jio_change_password'"), ajax.indexOf("add_action('wp_ajax_jio_change_email'"));
assert.match(password, /get_user_meta\(\$user_id, 'oauth_new', true\)/);
assert.match(password, /wp_check_password\(\$old/);
assert.match(password, /SecurityHardening::error_frequency_wait\(\)/);
assert.match(password, /SecurityHardening::error_frequency_record\(\)/);
assert.doesNotMatch(password, /aksrt_account_rate_limited/);
assert.doesNotMatch(ajax, /function aksrt_account_rate_limited/);
assert.doesNotMatch(ajax, /function aksrt_account_reset_rate/);
assert.match(password, /aksrt_require_captcha\('change_password'/);
assert.doesNotMatch(password, /jio_account_rate_limited/);
assert.doesNotMatch(ajax, /function jio_account_rate_limited/);
assert.doesNotMatch(ajax, /function jio_account_reset_rate/);
assert.match(password, /jio_require_captcha\('change_password'/);
assert.match(password, /Auth\\Service::validate_password\(\$password, \$password2\)/);
assert.doesNotMatch(password, /email.*code|password.*strength/i);
assert.match(javascript, /scene: 'resetpassword', target: target, channel:/);
@@ -6,17 +6,17 @@ declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
define('MINUTE_IN_SECONDS', 60);
$GLOBALS['aksrt_account_settings'] = ['email_set_captch' => true];
$GLOBALS['aksrt_account_user_id'] = 7;
$GLOBALS['aksrt_account_user'] = null;
$GLOBALS['aksrt_account_meta'] = [];
$GLOBALS['aksrt_account_transients'] = [];
$GLOBALS['aksrt_account_actions'] = [];
$GLOBALS['aksrt_account_mail'] = [];
$GLOBALS['aksrt_account_existing_emails'] = [];
$GLOBALS['aksrt_account_captcha_calls'] = [];
$GLOBALS['aksrt_account_captcha_fail'] = false;
$GLOBALS['aksrt_account_update_fail'] = false;
$GLOBALS['jio_account_settings'] = ['email_set_captch' => true];
$GLOBALS['jio_account_user_id'] = 7;
$GLOBALS['jio_account_user'] = null;
$GLOBALS['jio_account_meta'] = [];
$GLOBALS['jio_account_transients'] = [];
$GLOBALS['jio_account_actions'] = [];
$GLOBALS['jio_account_mail'] = [];
$GLOBALS['jio_account_existing_emails'] = [];
$GLOBALS['jio_account_captcha_calls'] = [];
$GLOBALS['jio_account_captcha_fail'] = false;
$GLOBALS['jio_account_update_fail'] = false;
final class WP_User {
public function __construct(
@@ -43,7 +43,7 @@ namespace {
}
function add_action(string $hook, callable $callback): void {
$GLOBALS['aksrt_account_actions'][$hook] = $callback;
$GLOBALS['jio_account_actions'][$hook] = $callback;
}
function __(string $text, string $domain = ''): string { return $text; }
function is_wp_error(mixed $value): bool { return $value instanceof WP_Error; }
@@ -52,64 +52,64 @@ namespace {
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
function sanitize_text_field(string $value): string { return trim(strip_tags($value)); }
function wp_unslash(mixed $value): mixed { return $value; }
function wp_verify_nonce(string $nonce, string $action): bool { return $nonce === 'valid' && $action === 'aksrt_account_settings'; }
function get_current_user_id(): int { return (int) $GLOBALS['aksrt_account_user_id']; }
function wp_get_current_user(): mixed { return $GLOBALS['aksrt_account_user']; }
function get_userdata(int $user_id): ?WP_User { return $user_id === 7 ? $GLOBALS['aksrt_account_user'] : null; }
function get_bloginfo(string $show = ''): string { return 'AKSRT Test'; }
function wp_verify_nonce(string $nonce, string $action): bool { return $nonce === 'valid' && $action === 'jio_account_settings'; }
function get_current_user_id(): int { return (int) $GLOBALS['jio_account_user_id']; }
function wp_get_current_user(): mixed { return $GLOBALS['jio_account_user']; }
function get_userdata(int $user_id): ?WP_User { return $user_id === 7 ? $GLOBALS['jio_account_user'] : null; }
function get_bloginfo(string $show = ''): string { return 'Jio Test'; }
function wp_hash_password(string $password): string { return 'hash:' . $password; }
function wp_check_password(string $password, string $hash, int $user_id = 0): bool { return hash_equals('hash:' . $password, $hash); }
function email_exists(string $email): int|false { return $GLOBALS['aksrt_account_existing_emails'][strtolower($email)] ?? false; }
function email_exists(string $email): int|false { return $GLOBALS['jio_account_existing_emails'][strtolower($email)] ?? false; }
function set_transient(string $key, mixed $value, int $expiration = 0): bool {
$GLOBALS['aksrt_account_transients'][$key] = [
$GLOBALS['jio_account_transients'][$key] = [
'value' => $value,
'expires' => $expiration > 0 ? time() + $expiration : 0,
];
return true;
}
function get_transient(string $key): mixed {
$row = $GLOBALS['aksrt_account_transients'][$key] ?? null;
$row = $GLOBALS['jio_account_transients'][$key] ?? null;
if (!is_array($row)) return false;
if (!empty($row['expires']) && $row['expires'] <= time()) {
unset($GLOBALS['aksrt_account_transients'][$key]);
unset($GLOBALS['jio_account_transients'][$key]);
return false;
}
return $row['value'] ?? false;
}
function delete_transient(string $key): bool {
unset($GLOBALS['aksrt_account_transients'][$key]);
unset($GLOBALS['jio_account_transients'][$key]);
return true;
}
function get_user_meta(int $user_id, string $key, bool $single = false): mixed {
return $GLOBALS['aksrt_account_meta'][$user_id][$key] ?? '';
return $GLOBALS['jio_account_meta'][$user_id][$key] ?? '';
}
function update_user_meta(int $user_id, string $key, mixed $value): bool {
$GLOBALS['aksrt_account_meta'][$user_id][$key] = $value;
$GLOBALS['jio_account_meta'][$user_id][$key] = $value;
return true;
}
function delete_user_meta(int $user_id, string $key): bool {
unset($GLOBALS['aksrt_account_meta'][$user_id][$key]);
unset($GLOBALS['jio_account_meta'][$user_id][$key]);
return true;
}
function wp_update_user(array $data): int|WP_Error {
if ($GLOBALS['aksrt_account_update_fail']) {
if ($GLOBALS['jio_account_update_fail']) {
return new WP_Error('update_failed', 'update failed');
}
$user = $GLOBALS['aksrt_account_user'];
$user = $GLOBALS['jio_account_user'];
if (isset($data['user_email'])) $user->user_email = (string) $data['user_email'];
if (isset($data['user_pass'])) $user->user_pass = wp_hash_password((string) $data['user_pass']);
return (int) $user->ID;
}
function aksrt_require_captcha(string $scene, array $payload): true|WP_Error {
$GLOBALS['aksrt_account_captcha_calls'][] = $scene;
return $GLOBALS['aksrt_account_captcha_fail'] ? new WP_Error('captcha_failed', 'captcha failed') : true;
function jio_require_captcha(string $scene, array $payload): true|WP_Error {
$GLOBALS['jio_account_captcha_calls'][] = $scene;
return $GLOBALS['jio_account_captcha_fail'] ? new WP_Error('captcha_failed', 'captcha failed') : true;
}
function aksrt_get_auth_agreement_view(): array { return ['enabled' => true, 'required' => true]; }
function jio_get_auth_agreement_view(): array { return ['enabled' => true, 'required' => true]; }
function wp_send_json_error(array $data): never { throw new JsonResponseException(['success' => false, 'data' => $data]); }
function wp_send_json_success(array $data): never { throw new JsonResponseException(['success' => true, 'data' => $data]); }
function account_settings_request(string $action, array $data = []): array {
$callback = $GLOBALS['aksrt_account_actions'][$action] ?? null;
$callback = $GLOBALS['jio_account_actions'][$action] ?? null;
if (!is_callable($callback)) throw new \RuntimeException('Missing AJAX action: ' . $action);
$_POST = array_merge(['_settings_nonce' => 'valid'], $data);
try {
@@ -125,15 +125,15 @@ namespace {
}
function account_settings_code_key(string $scene, string $target): string {
return 'aksrt_account_code_7_' . $scene . '_' . md5(strtolower(trim($target)));
return 'jio_account_code_7_' . $scene . '_' . md5(strtolower(trim($target)));
}
function account_settings_send_cooldown_key(string $target): string {
return 'aksrt_send_cd_' . md5(strtolower(trim($target)));
return 'jio_send_cd_' . md5(strtolower(trim($target)));
}
function account_settings_error_frequency_key(): string {
return 'aksrt_err_freq_test';
return 'jio_err_freq_test';
}
function account_settings_code(string $scene, string $target): string {
@@ -143,23 +143,23 @@ namespace {
}
function account_settings_proof_key(): string {
return 'aksrt_account_proof_email_old_7';
return 'jio_account_proof_email_old_7';
}
require dirname(__DIR__, 2) . '/inc/modules/user-parts/account-ajax.php';
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function get_all(): array { return $GLOBALS['aksrt_account_settings']; }
public static function get_all(): array { return $GLOBALS['jio_account_settings']; }
public static function send_event_email(string $event, string $to, array $values): bool {
$GLOBALS['aksrt_account_mail'][] = ['event' => $event, 'to' => $to, 'values' => $values];
$GLOBALS['jio_account_mail'][] = ['event' => $event, 'to' => $to, 'values' => $values];
return true;
}
}
}
namespace AKSRT\Auth {
namespace Jio\Auth {
final class Service {
public static function validate_password(string $password, string $confirm = ''): true|string {
if (strlen($password) < 6) return 'password too short';
@@ -169,52 +169,52 @@ namespace AKSRT\Auth {
}
}
namespace AKSRT\Extensions {
namespace Jio\Extensions {
final class SendLimit {
public static function check(string $target): array {
$sent_at = (int) get_transient('aksrt_send_cd_' . md5(strtolower(trim($target))));
$sent_at = (int) get_transient('jio_send_cd_' . md5(strtolower(trim($target))));
if ($sent_at > 0 && time() - $sent_at < 60) {
return ['ok' => false, 'message' => 'cooldown', 'remaining' => 60 - (time() - $sent_at)];
}
return ['ok' => true, 'message' => '', 'remaining' => 0];
}
public static function record(string $target): int {
set_transient('aksrt_send_cd_' . md5(strtolower(trim($target))), time(), 60);
set_transient('jio_send_cd_' . md5(strtolower(trim($target))), time(), 60);
return 60;
}
}
final class SecurityHardening {
public static function error_frequency_wait(): int {
$state = get_transient('aksrt_err_freq_test');
$state = get_transient('jio_err_freq_test');
$count = is_array($state) ? (int) ($state['count'] ?? 0) : 0;
if ($count < 5) return 0;
return max(0, ($count >= 10 ? 180 : 30) - (time() - (int) ($state['last'] ?? 0)));
}
public static function error_frequency_record(): void {
$state = get_transient('aksrt_err_freq_test');
$state = get_transient('jio_err_freq_test');
$count = is_array($state) ? (int) ($state['count'] ?? 0) + 1 : 1;
set_transient('aksrt_err_freq_test', ['count' => $count, 'last' => time()], 600);
set_transient('jio_err_freq_test', ['count' => $count, 'last' => time()], 600);
}
public static function error_frequency_clear(): void {
delete_transient('aksrt_err_freq_test');
delete_transient('jio_err_freq_test');
}
}
}
namespace {
$GLOBALS['aksrt_account_user'] = new WP_User(7, 'old@example.test', 'Test User', 'hash:OldPass123');
$send_action = 'wp_ajax_aksrt_send_account_email_code';
$verify_action = 'wp_ajax_aksrt_verify_account_email';
$change_email_action = 'wp_ajax_aksrt_change_email';
$change_password_action = 'wp_ajax_aksrt_change_password';
$saved_user = $GLOBALS['aksrt_account_user'];
$GLOBALS['aksrt_account_user'] = null;
$GLOBALS['jio_account_user'] = new WP_User(7, 'old@example.test', 'Test User', 'hash:OldPass123');
$send_action = 'wp_ajax_jio_send_account_email_code';
$verify_action = 'wp_ajax_jio_verify_account_email';
$change_email_action = 'wp_ajax_jio_change_email';
$change_password_action = 'wp_ajax_jio_change_password';
$saved_user = $GLOBALS['jio_account_user'];
$GLOBALS['jio_account_user'] = null;
account_settings_check(empty(account_settings_request($send_action, ['scene' => 'verify_email'])['success']), 'email-code request did not handle a missing user object');
account_settings_check(empty(account_settings_request($verify_action, ['code' => '123456'])['success']), 'old-email verification did not handle a missing user object');
account_settings_check(empty(account_settings_request($change_email_action, ['new_email' => 'new@example.test'])['success']), 'email update did not handle a missing user object');
account_settings_check(empty(account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123'])['success']), 'password update did not handle a missing user object');
$GLOBALS['aksrt_account_user'] = $saved_user;
$GLOBALS['jio_account_user'] = $saved_user;
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
account_settings_check(!empty($old_send['success']), 'old email verification code could not be sent');
@@ -226,79 +226,79 @@ namespace {
account_settings_check(empty($bad_old_code['success']), 'email verification accepted an incorrect old-email code');
$old_code_row = get_transient(account_settings_code_key('verify_email', 'old@example.test'));
account_settings_check(($old_code_row['attempts'] ?? 0) === 1, 'incorrect old-email attempt was not recorded');
$GLOBALS['aksrt_account_transients'][account_settings_code_key('verify_email', 'old@example.test')]['expires'] = time() - 1;
$GLOBALS['jio_account_transients'][account_settings_code_key('verify_email', 'old@example.test')]['expires'] = time() - 1;
$expired_old_code = account_settings_request($verify_action, ['code' => $old_code]);
account_settings_check(empty($expired_old_code['success']), 'email verification accepted an expired old-email code');
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'expired old-email code was not discarded');
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired old-email code issued an old-email proof');
account_settings_check(!jio_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired old-email code issued an old-email proof');
delete_transient(account_settings_send_cooldown_key('old@example.test'));
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
account_settings_check(!empty($old_send['success']), 'old email code could not be resent after expiry');
$old_code = account_settings_code('verify_email', 'old@example.test');
$verified_old = account_settings_request($verify_action, ['code' => $old_code]);
account_settings_check(!empty($verified_old['success']), 'email verification rejected the correct old-email code');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'old-email proof was not issued');
account_settings_check(jio_account_consume_proof(7, 'email_old', 'old@example.test', false), 'old-email proof was not issued');
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'valid old-email code was not consumed');
$new_email = 'new@example.test';
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
account_settings_check(!empty($new_send['success']), 'new email verification code could not be sent');
$new_code = account_settings_code('change_email', $new_email);
$GLOBALS['aksrt_account_existing_emails']['taken@example.test'] = 19;
$GLOBALS['jio_account_existing_emails']['taken@example.test'] = 19;
$duplicate = account_settings_request($change_email_action, ['new_email' => 'taken@example.test', 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(empty($duplicate['success']), 'email update accepted an address already owned by another account');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'duplicate-email validation consumed the old-email proof');
account_settings_check(jio_account_consume_proof(7, 'email_old', 'old@example.test', false), 'duplicate-email validation consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'duplicate-email validation consumed the new-email code');
unset($GLOBALS['aksrt_account_existing_emails']['taken@example.test']);
unset($GLOBALS['jio_account_existing_emails']['taken@example.test']);
$bad_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => 'wrong', 'agree_terms' => '1']);
account_settings_check(empty($bad_new_code['success']), 'email update accepted an incorrect new-email code');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'incorrect-code validation consumed the old-email proof');
account_settings_check(jio_account_consume_proof(7, 'email_old', 'old@example.test', false), 'incorrect-code validation consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'incorrect-code validation consumed the new-email code');
$GLOBALS['aksrt_account_transients'][account_settings_code_key('change_email', $new_email)]['expires'] = time() - 1;
$GLOBALS['jio_account_transients'][account_settings_code_key('change_email', $new_email)]['expires'] = time() - 1;
$expired_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(empty($expired_new_code['success']), 'email update accepted an expired new-email code');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired new-email code validation consumed the old-email proof');
account_settings_check(jio_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired new-email code validation consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'expired new-email code was not discarded');
delete_transient(account_settings_send_cooldown_key($new_email));
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
account_settings_check(!empty($new_send['success']), 'new email code could not be resent after expiry');
$new_code = account_settings_code('change_email', $new_email);
$GLOBALS['aksrt_account_update_fail'] = true;
$GLOBALS['jio_account_update_fail'] = true;
$write_failure = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(empty($write_failure['success']), 'email update reported success after the user write failed');
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'failed database write consumed the old-email proof');
account_settings_check(jio_account_consume_proof(7, 'email_old', 'old@example.test', false), 'failed database write consumed the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'failed database write consumed the new-email code');
$GLOBALS['aksrt_account_update_fail'] = false;
$GLOBALS['jio_account_update_fail'] = false;
$changed = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
account_settings_check(!empty($changed['success']), 'email update rejected valid old and new proofs');
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $new_email, 'email update did not persist the new address');
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'successful update retained the old-email proof');
account_settings_check($GLOBALS['jio_account_user']->user_email === $new_email, 'email update did not persist the new address');
account_settings_check(!jio_account_consume_proof(7, 'email_old', 'old@example.test', false), 'successful update retained the old-email proof');
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'successful update retained the new-email code');
account_settings_check((int) get_user_meta(7, 'aksrt_email_verified', true) === 1, 'successful update did not mark the email verified');
account_settings_check(end($GLOBALS['aksrt_account_mail'])['event'] === 'change_notice', 'successful update did not notify the previous email');
account_settings_check((int) get_user_meta(7, 'jio_email_verified', true) === 1, 'successful update did not mark the email verified');
account_settings_check(end($GLOBALS['jio_account_mail'])['event'] === 'change_notice', 'successful update did not notify the previous email');
$GLOBALS['aksrt_account_settings']['email_set_captch'] = false;
$GLOBALS['jio_account_settings']['email_set_captch'] = false;
$direct_email = account_settings_request($change_email_action, ['new_email' => 'direct@example.test', 'agree_terms' => '1']);
account_settings_check(!empty($direct_email['success']), 'email update without configured verification still required a code');
account_settings_check($GLOBALS['aksrt_account_user']->user_email === 'direct@example.test', 'direct email update did not persist');
account_settings_check($GLOBALS['jio_account_user']->user_email === 'direct@example.test', 'direct email update did not persist');
$GLOBALS['aksrt_account_settings']['email_set_captch'] = true;
$GLOBALS['aksrt_account_user']->user_email = '';
$GLOBALS['jio_account_settings']['email_set_captch'] = true;
$GLOBALS['jio_account_user']->user_email = '';
$first_email = 'first@example.test';
$first_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $first_email]);
account_settings_check(!empty($first_send['success']), 'first email binding did not send a new-address code');
$first_code = account_settings_code('change_email', $first_email);
$first_bind = account_settings_request($change_email_action, ['new_email' => $first_email, 'code' => $first_code, 'agree_terms' => '1']);
account_settings_check(!empty($first_bind['success']), 'first email binding incorrectly required old-email verification');
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $first_email, 'first email binding did not persist');
account_settings_check($GLOBALS['jio_account_user']->user_email === $first_email, 'first email binding did not persist');
$GLOBALS['aksrt_account_captcha_fail'] = true;
$GLOBALS['jio_account_captcha_fail'] = true;
$captcha_rejected = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
account_settings_check(empty($captcha_rejected['success']), 'password change bypassed human verification');
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:OldPass123', 'captcha rejection changed the password');
$GLOBALS['aksrt_account_captcha_fail'] = false;
account_settings_check($GLOBALS['jio_account_user']->user_pass === 'hash:OldPass123', 'captcha rejection changed the password');
$GLOBALS['jio_account_captcha_fail'] = false;
for ($attempt = 0; $attempt < 8; $attempt++) {
$wrong_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'wrong']);
@@ -309,7 +309,7 @@ namespace {
delete_transient(account_settings_error_frequency_key());
$changed_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
account_settings_check(!empty($changed_password['success']), 'password change rejected the correct old password');
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:NewPass123', 'password change did not persist the new password');
account_settings_check($GLOBALS['jio_account_user']->user_pass === 'hash:NewPass123', 'password change did not persist the new password');
account_settings_check(get_transient(account_settings_error_frequency_key()) === false, 'successful password change did not reset its error limit');
update_user_meta(7, 'oauth_new', 1);
+2 -2
View File
@@ -12,8 +12,8 @@ assert.equal(core.dependencyVisible(['mode', '!=', 'cards'], 'cards'), false);
assert.equal(core.dependencyVisible([], 'anything'), true);
assert.equal(
core.replaceRepeaterIndexes('widget-aksrt_widget_slider[4][slides][__index_ab12cd34__][layers][9][title]', [2, 1]),
'widget-aksrt_widget_slider[4][slides][2][layers][1][title]'
core.replaceRepeaterIndexes('widget-jio_widget_slider[4][slides][__index_ab12cd34__][layers][9][title]', [2, 1]),
'widget-jio_widget_slider[4][slides][2][layers][1][title]'
);
assert.equal(core.replaceRepeaterIndexes('plain[name]', [1]), 'plain[name]');
@@ -7,8 +7,8 @@ const root = path.resolve(__dirname, '../..')
const read = file => fs.readFileSync(path.join(root, file), 'utf8')
const list = read('src/Admin/Native/PostListEnhance.php')
const tags = read('inc/helpers/aksrt-frontend-tags.php')
const pay = read('inc/helpers/aksrt-pay.php')
const tags = read('inc/helpers/jio-frontend-tags.php')
const pay = read('inc/helpers/jio-pay.php')
test('admin post list has the interaction stats column', () => {
assert.match(list, /manage_post_posts_columns/)
@@ -20,7 +20,7 @@ test('admin post list has the interaction stats column', () => {
test('admin post list has the sales data column with a sortable header', () => {
// 表头「销售数据」必须是可点击排序链接,排序键为 sales_volume。
assert.match(list, /销售数据/)
assert.match(list, /'aksrt_sales'/)
assert.match(list, /'jio_sales'/)
assert.match(list, /orderby.*sales_volume/)
// 排序靠表头 label 里的链接 + pre_get_posts 落地,不注册 sortable_columns:
@@ -38,25 +38,25 @@ test('sales column renders pay type, price and both sales counts', () => {
assert.match(list, /pay_type_labels\(\)/)
assert.match(list, /真实销量/)
assert.match(list, /get_content_sales_map/)
// 第一行取自 _aksrt_pay,第二行的销量取自 _aksrt_sales_volume。
assert.match(list, /aksrt_get_pay_meta/)
// 第一行取自 _jio_pay,第二行的销量取自 _jio_sales_volume。
assert.match(list, /jio_get_pay_meta/)
assert.match(list, /DomainModel::RESOURCE_SALES_VOLUME_KEY/)
// 真实销量链接必须跳到订单页(aksrt_orders)并按商品 ID 过滤。
assert.match(list, /'aksrt_orders'/)
// 真实销量链接必须跳到订单页(jio_orders)并按商品 ID 过滤。
assert.match(list, /'jio_orders'/)
assert.match(list, /'product_id'/)
})
test('interaction counts are read from the keys the theme actually writes', () => {
// 计数一律落在这几个 meta key 上:aksrt_post_views / _aksrt_like_count /
// _aksrt_favorite_count。列回调只能用同一套键,否则全列恒为 0。
// 计数一律落在这几个 meta key 上:jio_post_views / _jio_like_count /
// _jio_favorite_count。列回调只能用同一套键,否则全列恒为 0。
// 互动数据不做下载量统计,因此不再读取任何 download 键。
assert.match(tags, /'_aksrt_like_count'/)
assert.match(tags, /'_aksrt_favorite_count'/)
assert.match(tags, /'aksrt_post_views'/)
assert.match(tags, /'_jio_like_count'/)
assert.match(tags, /'_jio_favorite_count'/)
assert.match(tags, /'jio_post_views'/)
assert.match(list, /aksrt_get_post_views\(\$post_id\)/)
assert.match(list, /aksrt_get_post_likes\(\$post_id\)/)
assert.match(list, /aksrt_get_post_favorites\(\$post_id\)/)
assert.match(list, /jio_get_post_views\(\$post_id\)/)
assert.match(list, /jio_get_post_likes\(\$post_id\)/)
assert.match(list, /jio_get_post_favorites\(\$post_id\)/)
// 不得再直接读这些不存在的键。
const column = list.slice(list.indexOf('private function render_stats_column'), list.indexOf('private function render_sales_column'))
@@ -71,17 +71,17 @@ test('interaction counts are read from the keys the theme actually writes', () =
assert.doesNotMatch(column, /下载/)
})
test('admin stats column shares the aksrt_format_number abbreviation', () => {
test('admin stats column shares the jio_format_number abbreviation', () => {
// 后台列与前台必须同一套缩写口径(≥10000 → 1.2W+,≥1000 → 1.5K)。
assert.match(list, /aksrt_format_number\(\$n\)/)
assert.match(list, /jio_format_number\(\$n\)/)
const column = list.slice(list.indexOf('private function render_stats_column'), list.indexOf('private function render_sales_column'))
assert.doesNotMatch(column, /'万'/)
})
test('sortable meta maps the public orderby keys to the stored keys', () => {
assert.match(list, /'views'\s*=>\s*\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/)
assert.match(list, /'like'\s*=>\s*'_aksrt_like_count'/)
assert.match(list, /'favorite'\s*=>\s*'_aksrt_favorite_count'/)
assert.match(list, /'views'\s*=>\s*\\Jio\\Core\\DomainModel::POST_VIEW_COUNT_KEY/)
assert.match(list, /'like'\s*=>\s*'_jio_like_count'/)
assert.match(list, /'favorite'\s*=>\s*'_jio_favorite_count'/)
// meta_key 必须是真实存储键,而不是用户可见的排序键。
assert.match(list, /\$query->set\('meta_key', \$this->stats_meta_key\(\$orderby\)\)/)
// 互动数据不统计下载量,排序映射里也不得再出现该维度。
@@ -90,21 +90,21 @@ test('sortable meta maps the public orderby keys to the stored keys', () => {
test('every counter meta key the admin column renders has a writer', () => {
// 逐键核对:列里展示的每项计数都必须在某个 helper 里真正被写入过。
// _aksrt_sales_volume 的写入方是付费订单流水(PayOrders),不在这份清单里,
// _jio_sales_volume 的写入方是付费订单流水(PayOrders),不在这份清单里,
// 由下一个用例单独覆盖。
const sources = [
'src/Admin/Native/PostListEnhance.php',
'inc/helpers/aksrt-frontend-tags.php',
'inc/helpers/jio-frontend-tags.php',
'src/Core/PostMetaBox.php',
'src/Frontend/PageAdminSet.php',
].map(read).join('\n')
for (const key of ['aksrt_post_views', '_aksrt_like_count', '_aksrt_favorite_count']) {
for (const key of ['jio_post_views', '_jio_like_count', '_jio_favorite_count']) {
assert.ok(sources.includes(`'${key}'`), `${key} is written somewhere`)
}
// 资源帖的浏览有独立键,aksrt_get_post_views() 取两者较大值。
assert.match(tags, /_aksrt_res_view_count/)
assert.match(read('inc/helpers/aksrt-resource.php'), /RESOURCE_VIEW_COUNT_KEY|_aksrt_res_view_count/)
// 资源帖的浏览有独立键,jio_get_post_views() 取两者较大值。
assert.match(tags, /_jio_res_view_count/)
assert.match(read('inc/helpers/jio-resource.php'), /RESOURCE_VIEW_COUNT_KEY|_jio_res_view_count/)
// 互动数据不统计下载量:列里不得再出现下载键的读取或回退逻辑。
assert.doesNotMatch(list, /RESOURCE_DOWNLOAD_COUNT_KEY/)
@@ -112,14 +112,14 @@ test('every counter meta key the admin column renders has a writer', () => {
})
test('sales volume is written by the order pipeline', () => {
// _aksrt_sales_volume 的写入方在付费助手与订单处理器里,列本身只读。
assert.match(pay, /aksrt_bump_content_sales_volume/)
// _jio_sales_volume 的写入方在付费助手与订单处理器里,列本身只读。
assert.match(pay, /jio_bump_content_sales_volume/)
assert.match(pay, /RESOURCE_SALES_VOLUME_KEY/)
assert.match(read('src/Content/PayOrders.php'), /aksrt_bump_content_sales_volume/)
assert.match(read('src/Content/PayOrders.php'), /jio_bump_content_sales_volume/)
})
test('admin column icons stay behind the Font Awesome enqueue on edit.php', () => {
assert.match(list, /admin_enqueue_scripts[\s\S]*?edit\.php[\s\S]*?aksrt_enqueue_fontawesome\(\)/)
assert.match(list, /admin_enqueue_scripts[\s\S]*?edit\.php[\s\S]*?jio_enqueue_fontawesome\(\)/)
for (const klass of ['fa-eye', 'fa-thumbs-up', 'fa-star']) {
assert.match(list, new RegExp(klass))
}
@@ -131,7 +131,7 @@ test('the old closure-based column implementation is gone from core-hooks', () =
const hooks = read('inc/core-hooks.php')
assert.doesNotMatch(hooks, /manage_post_posts_columns/)
assert.doesNotMatch(hooks, /manage_post_posts_custom_column/)
assert.doesNotMatch(hooks, /aksrt_stats/)
assert.doesNotMatch(hooks, /jio_stats/)
})
test('PostListEnhance is wired up from functions.php', () => {
+22 -22
View File
@@ -8,21 +8,21 @@ const read = file => fs.readFileSync(path.join(root, file), 'utf8')
const app = read('assets/admin/src/App.vue')
const styles = read('assets/admin/src/style.css')
const field = read('assets/admin/src/components/form/AksField.vue')
const fieldGroup = read('assets/admin/src/components/form/AksFieldGroup.vue')
const formRenderer = read('assets/admin/src/components/form/AksFormRenderer.vue')
const saveBar = read('assets/admin/src/components/form/AksSaveBar.vue')
const field = read('assets/admin/src/components/form/JioField.vue')
const fieldGroup = read('assets/admin/src/components/form/JioFieldGroup.vue')
const formRenderer = read('assets/admin/src/components/form/JioFormRenderer.vue')
const saveBar = read('assets/admin/src/components/form/JioSaveBar.vue')
const schemaProvider = read('src/Admin/SchemaProvider.php')
test('admin theme exposes the approved white, grey, and green tokens', () => {
const requiredTokens = {
'--aksrt-color-primary': '#18A058',
'--aksrt-color-primary-soft': '#EAF7EF',
'--aksrt-color-canvas': '#F5F7F6',
'--aksrt-color-surface': '#FFFFFF',
'--aksrt-color-text': '#1F2923',
'--aksrt-color-muted': '#6F7A72',
'--aksrt-color-border': '#E2E8E3',
'--jio-color-primary': '#18A058',
'--jio-color-primary-soft': '#EAF7EF',
'--jio-color-canvas': '#F5F7F6',
'--jio-color-surface': '#FFFFFF',
'--jio-color-text': '#1F2923',
'--jio-color-muted': '#6F7A72',
'--jio-color-border': '#E2E8E3',
}
for (const [token, value] of Object.entries(requiredTokens)) {
@@ -35,27 +35,27 @@ test('app uses Naive UI overrides and the compact responsive shell', () => {
assert.match(app, /const adminThemeOverrides = \{/)
assert.match(app, /:width="232"/)
assert.match(app, /const MOBILE_BREAKPOINT = 960/)
assert.match(app, /aksrt-logo__version/)
assert.doesNotMatch(app, /aksrt-csf-(?:shell|header|version)/)
assert.match(app, /jio-logo__version/)
assert.doesNotMatch(app, /jio-csf-(?:shell|header|version)/)
})
test('rejected gradient banner and split-row form overrides do not return', () => {
const source = `${app}\n${styles}`
assert.doesNotMatch(source, /#f97794|#623aa2|#0991f5|#0997f5/i)
assert.doesNotMatch(source, /linear-gradient/i)
assert.doesNotMatch(styles, /\.aksrt-field\s*>\s*:not/)
assert.doesNotMatch(styles, /\.aksrt-field__label-row\s*\{[^}]*grid-column/s)
assert.doesNotMatch(styles, /\.jio-field\s*>\s*:not/)
assert.doesNotMatch(styles, /\.jio-field__label-row\s*\{[^}]*grid-column/s)
})
test('shared forms use card surfaces and design tokens', () => {
assert.match(fieldGroup, /border:\s*1px solid var\(--aksrt-color-border\)/)
assert.match(fieldGroup, /border:\s*1px solid var\(--jio-color-border\)/)
assert.match(fieldGroup, /border-radius:\s*12px/)
assert.match(fieldGroup, /box-shadow:\s*var\(--aksrt-shadow-card\)/)
assert.match(field, /color:\s*var\(--aksrt-color-primary\)/)
assert.match(field, /background:\s*var\(--aksrt-color-primary-soft\)/)
assert.match(fieldGroup, /box-shadow:\s*var\(--jio-shadow-card\)/)
assert.match(field, /color:\s*var\(--jio-color-primary\)/)
assert.match(field, /background:\s*var\(--jio-color-primary-soft\)/)
// Sticky save bar is shared by schema pages and custom settings pages.
assert.match(formRenderer, /<AksSaveBar /)
assert.match(saveBar, /background:\s*var\(--aksrt-color-surface\)/)
assert.match(formRenderer, /<JioSaveBar /)
assert.match(saveBar, /background:\s*var\(--jio-color-surface\)/)
})
test('navigation, focus, responsive layout, and reduced motion are contracted', () => {
@@ -87,7 +87,7 @@ test('captcha scene whitelist matches both admin schemas and excludes dead email
assert.doesNotMatch(phpSchema, /'verify_email'/)
assert.doesNotMatch(jsSchema, /value:\s*'verify_email'/)
const sceneBlock = captcha.match(/function aksrt_captcha_scenes\(\): array \{[\s\S]*?return \[([\s\S]*?)\n\t\];/)
const sceneBlock = captcha.match(/function jio_captcha_scenes\(\): array \{[\s\S]*?return \[([\s\S]*?)\n\t\];/)
const phpField = phpSchema.match(/'key' => 'enabled_scenarios'[\s\S]*?'options' => \[([\s\S]*?)\]\],/)
const jsField = jsSchema.match(/key: 'enabled_scenarios'[\s\S]*?options: \[([\s\S]*?)\n\s*\],/)
assert.ok(sceneBlock && phpField && jsField, 'captcha scene contracts must be discoverable')
@@ -32,12 +32,12 @@ test('old asset implementations and compatibility aliases are absent', () => {
...sourceFiles(path.join(root, 'templates')),
]
const forbidden = [
/aksrt_credits(?:\b|_)/,
/['"]aksrt_balance['"]/,
/jio_credits(?:\b|_)/,
/['"]jio_balance['"]/,
/\bcredit_log\b/,
/\bbalance_log\b/,
/aksrt_credits_(?:added|deducted)/,
/aksrt_balance_(?:added|deducted)/,
/jio_credits_(?:added|deducted)/,
/jio_balance_(?:added|deducted)/,
/credits_(?:added|deducted)/,
// points_pass_exchange_* 现为正式设置键(卡密兑换积分功能,对齐参考主题命名),
// 由 card-pass-modal-redeem.test.cjs 覆盖,不再作为遗留别名禁用。
@@ -49,7 +49,7 @@ test('old asset implementations and compatibility aliases are absent', () => {
/\brebate_log\b/,
/\/payment\/rebate(?:\b|\/)/,
/\bpay_rebate\b/,
/aksrtpay_rebate_page/,
/jiopay_rebate_page/,
]
const violations = []
for (const file of files) {
@@ -84,11 +84,11 @@ test('skeleton loading ships one contract and no dead spinner stylesheets', () =
for (const file of css) {
const text = fs.readFileSync(file, 'utf8')
assert.doesNotMatch(text, /\.load-more__(?:btn|spinner)/, `${path.relative(root, file)} must not reintroduce .load-more__*`)
assert.doesNotMatch(text, /\.aksrt-loading__(?:spinner|overlay|inline|text|dot)/, `${path.relative(root, file)} must not reintroduce .aksrt-loading__*`)
assert.doesNotMatch(text, /aksrt-progress-bar/, `${path.relative(root, file)} must not reintroduce .aksrt-progress-bar`)
assert.doesNotMatch(text, /\.jio-loading__(?:spinner|overlay|inline|text|dot)/, `${path.relative(root, file)} must not reintroduce .jio-loading__*`)
assert.doesNotMatch(text, /jio-progress-bar/, `${path.relative(root, file)} must not reintroduce .jio-progress-bar`)
// A second parallel skeleton system (never wired to any producer).
assert.doesNotMatch(text, /\.aksrt-skeleton/, `${path.relative(root, file)} must not reintroduce .aksrt-skeleton-*`)
assert.doesNotMatch(text, /aksrt-skeleton-shimmer/, `${path.relative(root, file)} must not reintroduce the parallel shimmer keyframes`)
assert.doesNotMatch(text, /\.jio-skeleton/, `${path.relative(root, file)} must not reintroduce .jio-skeleton-*`)
assert.doesNotMatch(text, /jio-skeleton-shimmer/, `${path.relative(root, file)} must not reintroduce the parallel shimmer keyframes`)
assert.doesNotMatch(text, /--skeleton-(?:base|shine)/, `${path.relative(root, file)} must not reintroduce the unused skeleton tokens`)
}
})
@@ -162,18 +162,18 @@ test('skeleton blocks share the card gutter instead of overflowing it', () => {
// column. It must span every column and re-run the same 2-column template
// internally while loading, so the placeholders line up with the real cards.
const userHome = fs.readFileSync(path.join(root, 'assets/css/routes/user-home.css'), 'utf8')
assert.match(userHome, /\.aksrt-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \.posts-item \{\s*margin-right: 0;\s*margin-left: 0;/)
assert.match(userHome, /\.aksrt-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \{\s*grid-column: 1 \/ -1;/)
assert.match(userHome, /\.jio-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \.posts-item \{\s*margin-right: 0;\s*margin-left: 0;/)
assert.match(userHome, /\.jio-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \{\s*grid-column: 1 \/ -1;/)
assert.match(
userHome,
/\.dashboard-panel\.is-loading \.aksrt-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \{[^}]*display: grid;[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/s
/\.dashboard-panel\.is-loading \.jio-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \{[^}]*display: grid;[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/s
)
})
test('asset tables are InnoDB and operation keys are unique', () => {
const schema = fs.readFileSync(path.join(root, 'src', 'Core', 'DBMigratorPart2.php'), 'utf8')
for (const table of ['asset_account', 'asset_transaction', 'asset_entry']) {
assert.match(schema, new RegExp(`CREATE TABLE \\{\\$prefix\\}aksrt_${table} \\([\\s\\S]*?\\) ENGINE=InnoDB`))
assert.match(schema, new RegExp(`CREATE TABLE \\{\\$prefix\\}jio_${table} \\([\\s\\S]*?\\) ENGINE=InnoDB`))
}
assert.match(schema, /UNIQUE KEY uniq_operation_key \(operation_key\)/)
assert.match(schema, /UNIQUE KEY uniq_transaction_line \(transaction_id, line_no\)/)
@@ -207,12 +207,12 @@ test('asset record tabs and pagination stay in the DOM without URL state', () =>
const credits = fs.readFileSync(path.join(root, 'assets', 'js', 'credits.js'), 'utf8')
const center = fs.readFileSync(path.join(root, 'src', 'User', 'Center.php'), 'utf8')
assert.match(profile, /data-aksrt-asset-tab="balance"/)
assert.match(profile, /data-aksrt-asset-tab="points"/)
assert.match(points, /aksrt_render_asset_pagination\([^;]+, 'points'\)/)
assert.match(wallet, /aksrt_render_asset_pagination\([^;]+, 'balance'\)/)
assert.match(profile, /data-jio-asset-tab="balance"/)
assert.match(profile, /data-jio-asset-tab="points"/)
assert.match(points, /jio_render_asset_pagination\([^;]+, 'points'\)/)
assert.match(wallet, /jio_render_asset_pagination\([^;]+, 'balance'\)/)
assert.doesNotMatch(profile + points + wallet, /\$_GET\[['"](?:wallet_page|credits_page)['"]\]/)
assert.match(credits, /data-aksrt-asset-page/)
assert.match(credits, /data-jio-asset-page/)
assert.match(credits, /user-center\/panel\/credits\?asset=/)
assert.match(center, /'asset'\s*=>\s*\[/)
assert.match(center, /\$context\['asset_request'\]/)
@@ -256,15 +256,15 @@ test('points orders bypass cash commissions and purchase dead paths stay removed
assert.ok(pointsGuard >= 0 && affiliateGuard > pointsGuard)
assert.match(paidMethod, /\$currency\s*=\s*sanitize_key/)
const pay = fs.readFileSync(path.join(root, 'inc', 'helpers', 'aksrt-pay.php'), 'utf8')
const payTypesStart = pay.indexOf('function aksrt_get_pay_types')
const cashMethodsStart = pay.indexOf('function aksrt_list_cash_payment_methods', payTypesStart)
const pay = fs.readFileSync(path.join(root, 'inc', 'helpers', 'jio-pay.php'), 'utf8')
const payTypesStart = pay.indexOf('function jio_get_pay_types')
const cashMethodsStart = pay.indexOf('function jio_list_cash_payment_methods', payTypesStart)
const payTypesMethod = pay.slice(payTypesStart, cashMethodsStart)
assert.ok(payTypesStart >= 0 && cashMethodsStart > payTypesStart)
assert.equal((payTypesMethod.match(/(?:^|\n)\s*return\b/g) || []).length, 1)
const cashier = fs.readFileSync(path.join(root, 'assets', 'js', 'cms-download.js'), 'utf8')
assert.match(cashier, /\[data-action="aksrt-need-purchase"\], \[data-aksrt-pay-content\]/)
assert.match(cashier, /\[data-action="jio-need-purchase"\], \[data-jio-pay-content\]/)
assert.doesNotMatch(cashier, /const cashierLink\s*=\s*e\.target\.closest\('\.cashier-link'\)/)
})
@@ -356,7 +356,7 @@ test('asset purchases share the order center and refresh every wallet surface',
const chargeCard = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), 'utf8')
const creditsJs = fs.readFileSync(path.join(root, 'assets', 'js', 'credits.js'), 'utf8')
const sidebar = fs.readFileSync(path.join(root, 'inc', 'helpers', 'user-center-renderers', 'sidebar-commerce.php'), 'utf8')
const header = fs.readFileSync(path.join(root, 'inc', 'helpers', 'aksrt-header-userbox.php'), 'utf8')
const header = fs.readFileSync(path.join(root, 'inc', 'helpers', 'jio-header-userbox.php'), 'utf8')
assert.match(domain, /ORDER_PRODUCT_POINTS\s*=\s*'points'/)
const productTypes = ['READING', 'DOWNLOAD', 'IMAGE', 'VIDEO', 'VIP', 'RECHARGE', 'POINTS']
@@ -370,12 +370,12 @@ test('asset purchases share the order center and refresh every wallet surface',
statuses.map((status) => [status, status.toLowerCase()]),
)
assert.deepEqual(
[...order.matchAll(/public const STATUS_([A-Z_]+)\s*=\s*\\AKSRT\\Core\\DomainModel::ORDER_STATUS_([A-Z_]+);/g)].map((match) => [match[1], match[2]]),
[...order.matchAll(/public const STATUS_([A-Z_]+)\s*=\s*\\Jio\\Core\\DomainModel::ORDER_STATUS_([A-Z_]+);/g)].map((match) => [match[1], match[2]]),
statuses.map((status) => [status, status]),
)
for (const status of statuses) {
assert.match(domain, new RegExp(`ORDER_STATUS_${status}\\s*=\\s*'${status.toLowerCase()}'`))
assert.match(order, new RegExp(`STATUS_${status}\\s*=\\s*\\\\AKSRT\\\\Core\\\\DomainModel::ORDER_STATUS_${status}`))
assert.match(order, new RegExp(`STATUS_${status}\\s*=\\s*\\\\Jio\\\\Core\\\\DomainModel::ORDER_STATUS_${status}`))
assert.match(query, new RegExp(`STATUS_${status}\\s*=\\s*'${status.toLowerCase()}'`))
}
for (const status of ['CREATED', 'DELIVERED', 'COMPLETED', 'PENDING_REVIEW', 'CANCELLED', 'EXPIRED']) {
@@ -401,7 +401,7 @@ test('asset purchases share the order center and refresh every wallet surface',
assert.match(repository, /SUM\(CASE WHEN status = 'refunding' THEN 1 ELSE 0 END\) AS paid_legacy/)
assert.match(repository, /'paid' => \(int\) \(\$row\['paid'\] \?\? 0\) \+ \(int\) \(\$row\['paid_legacy'\] \?\? 0\)/)
assert.doesNotMatch(repository, /\$completed_statuses|\$completed_placeholders/)
assert.match(adminRepository, /\$valid_statuses = array_keys\(\\AKSRT\\Core\\DomainModel::order_status_labels\(\)\)/)
assert.match(adminRepository, /\$valid_statuses = array_keys\(\\Jio\\Core\\DomainModel::order_status_labels\(\)\)/)
assert.doesNotMatch(payOrders, /<select name="new_status">/)
assert.doesNotMatch(`${order}\n${repository}\n${userOrders}\n${payOrders}`, /['"]force['"]\s*=>\s*true/)
assert.match(paymentRuntime, /\$status_keys = \['pending', 'paid', 'refunded', 'closed'\]/)
@@ -410,8 +410,8 @@ test('asset purchases share the order center and refresh every wallet surface',
}
assert.doesNotMatch(paymentSchema, /payment_order_status_(?:completed|cancelled|processing)/)
assert.match(balanceRest, /register_rest_route\('aksrt\/v1', '\/pay\/assets'/)
assert.match(balanceRest, /'product_type'\s*=>\s*\\AKSRT\\Core\\DomainModel::ORDER_PRODUCT_POINTS/)
assert.match(balanceRest, /register_rest_route\('jio\/v1', '\/pay\/assets'/)
assert.match(balanceRest, /'product_type'\s*=>\s*\\Jio\\Core\\DomainModel::ORDER_PRODUCT_POINTS/)
assert.match(balanceRest, /\$existing_order\s*=\s*\$repo->get_by_request_key\(\$operation_key\)/)
assert.match(balanceRest, /\$order_number\s*=\s*\$repo->allocate_order_number\(\)/)
assert.doesNotMatch(balanceRest, /['"]PX['"]\s*\.|hash\('sha256',\s*\$operation_key\)/)
@@ -420,15 +420,15 @@ test('asset purchases share the order center and refresh every wallet surface',
assert.match(chargeCard, /self::TYPE_CREDITS[\s\S]*?ORDER_PRODUCT_POINTS/)
assert.match(creditsJs, /function refreshAssetBalances\(/)
assert.match(creditsJs, /querySelectorAll\('\[data-aksrt-wallet-value\]'/)
assert.match(creditsJs, /querySelectorAll\('\[data-aksrt-points-value\]'/)
assert.match(sidebar, /data-aksrt-wallet-value/)
assert.match(sidebar, /data-aksrt-points-value/)
assert.match(header, /data-aksrt-wallet-value/)
assert.match(header, /data-aksrt-points-value/)
assert.match(header, /data-aksrt-wallet-value data-aksrt-count-format="cut-count"/)
assert.match(header, /data-aksrt-points-value data-aksrt-count-format="cut-count"/)
assert.match(creditsJs, /element\.dataset\.aksrtCountFormat === 'cut-count' \? formatCutCount\(/)
assert.match(creditsJs, /querySelectorAll\('\[data-jio-wallet-value\]'/)
assert.match(creditsJs, /querySelectorAll\('\[data-jio-points-value\]'/)
assert.match(sidebar, /data-jio-wallet-value/)
assert.match(sidebar, /data-jio-points-value/)
assert.match(header, /data-jio-wallet-value/)
assert.match(header, /data-jio-points-value/)
assert.match(header, /data-jio-wallet-value data-jio-count-format="cut-count"/)
assert.match(header, /data-jio-points-value data-jio-count-format="cut-count"/)
assert.match(creditsJs, /element\.dataset\.jioCountFormat === 'cut-count' \? formatCutCount\(/)
})
test('header wallet count formatting matches the server cut-count thresholds', () => {
@@ -450,9 +450,9 @@ test('recharge completion always credits the ledger and expiry SQL is strict-mod
const charge = fs.readFileSync(path.join(root, 'src', 'Credits', 'BalanceServiceCharge.php'), 'utf8')
const gateway = fs.readFileSync(path.join(root, 'src', 'Payment', 'Gateway.php'), 'utf8')
assert.match(repository, /\$new_status === \\AKSRT\\Core\\DomainModel::ORDER_STATUS_PAID/)
assert.match(charge, /'status'\s*=>\s*\\AKSRT\\Payment\\Order::STATUS_PAID/)
assert.match(charge, /\\AKSRT\\Payment\\Order::STATUS_PENDING,\s*\\AKSRT\\Payment\\Order::STATUS_PAID/)
assert.match(repository, /\$new_status === \\Jio\\Core\\DomainModel::ORDER_STATUS_PAID/)
assert.match(charge, /'status'\s*=>\s*\\Jio\\Payment\\Order::STATUS_PAID/)
assert.match(charge, /\\Jio\\Payment\\Order::STATUS_PENDING,\s*\\Jio\\Payment\\Order::STATUS_PAID/)
assert.doesNotMatch(charge, /Order::STATUS_(?:CREATED|COMPLETED)/)
assert.match(gateway, /ORDER_PRODUCT_RECHARGE[\s\S]*?update_status\(\$order_number, Order::STATUS_PAID/)
assert.doesNotMatch(repository, /expires_at\s*<>\s*''/)
@@ -22,7 +22,7 @@ test('modal and sign page use one renderer and one state machine', () => {
assert.match(shared, /data-auth-panel="bind"/);
assert.match(shared, /data-auth-panel="oauth"/);
assert.match(scripts, /assets\/js\/auth\.js/);
assert.match(scripts, /'aksrt-auth',[\s\S]*?assets\/js\/auth\.js[\s\S]*?\['loader_js'\]/);
assert.match(scripts, /'jio-auth',[\s\S]*?assets\/js\/auth\.js[\s\S]*?\['loader_js'\]/);
assert.doesNotMatch(scripts, /modal-auth\.js|sign-register\.js/);
assert.doesNotMatch(runtime, /signsubmit-loader|tbquire\(\['sign-register'\]\)|\.on\('click', '\.(?:signin|signup)-loader'/);
assert.doesNotMatch(loader, /sign-register/);
@@ -56,15 +56,15 @@ test('authentication settings use reference defaults and canonical keys', () =>
});
test('authentication galleries save attachment IDs and are consumed by both surfaces', () => {
const field = read('assets/admin/src/components/form/AksField.vue');
const gallery = read('assets/admin/src/components/form/AksGalleryField.vue');
const field = read('assets/admin/src/components/form/JioField.vue');
const gallery = read('assets/admin/src/components/form/JioGalleryField.vue');
const provider = read('src/Admin/SchemaProvider.php');
const reader = read('src/Settings/AuthReader.php');
const modal = read('templates/parts/modal-auth.php');
const page = read('templates/pages/page-sign.php');
const css = read('assets/css/routes/sign.css');
assert.match(field, /<AksGalleryField[\s\S]*?schema\.type === 'gallery'/);
assert.match(field, /<JioGalleryField[\s\S]*?schema\.type === 'gallery'/);
assert.match(gallery, /multiple:\s*true/);
assert.match(gallery, /Number\.parseInt\(attachment\.id/);
assert.match(provider, /'gallery'\s*=>\s*'attachment_ids'/);
@@ -88,10 +88,10 @@ test('only the shared authentication REST routes and /sign/ renderer are registe
assert.match(controller, new RegExp(route.replace('/', '\\/')));
});
assert.doesNotMatch(controller, /auth\/(?:modal-|phone-|send-sms-code|bind-phone)/);
assert.match(router, /'sign'\s*=>\s*'index\.php\?aksrt_sign=1'/);
assert.match(router, /'sign'\s*=>\s*'index\.php\?jio_sign=1'/);
assert.doesNotMatch(router, /'login'\s*=>|'register'\s*=>|'reset-password'\s*=>|'verify-email'\s*=>|'bind-account'\s*=>/);
assert.match(router, /aksrt_bypass/);
assert.match(router, /\$action === 'logout'[\s\S]*?aksrt_get_logout_url/);
assert.match(router, /jio_bypass/);
assert.match(router, /\$action === 'logout'[\s\S]*?jio_get_logout_url/);
assert.doesNotMatch(router, /is_user_logged_in\(\) && current_user_can\('manage_options'\)|wp_doing_ajax\(\)/);
assert.match(urls, /home_url\('\/sign\/'\)/);
});
@@ -136,7 +136,7 @@ test('unified handlers enforce nonce, captcha, rate limits, configured channels,
assert.match(handlers, /账号或密码错误/);
assert.match(handlers, /账号或验证码错误/);
assert.match(service, /signin_phone_enabled/);
assert.doesNotMatch(service + serviceA + serviceB, /aksrt_require_captcha/);
assert.doesNotMatch(service + serviceA + serviceB, /jio_require_captcha/);
assert.doesNotMatch(service + serviceA + serviceB, /_auth_code_verified|_defer_login/);
assert.doesNotMatch(service, /captcha_scene/);
assert.match(handlers, /public function user_bind_phone[\s\S]*?auth_require_captcha\('bind_phone'/);
@@ -177,7 +177,7 @@ test('shared renderer contracts every configured authentication state', () => {
assert.match(shared, /oauth-button-lg/);
assert.match(shared, /data-auth-qrcode/);
assert.match(modal, /data-auto=/);
assert.match(client, /aksrtAuthAutoUntil/);
assert.match(client, /jioAuthAutoUntil/);
assert.match(client, /data-password-show/);
assert.match(client, /pollQr/);
assert.match(client, /AbortController/);
@@ -205,8 +205,8 @@ test('OAuth frontend is accessible, redirect-aware, and removes retired provider
const signCss = read('assets/css/routes/sign.css');
const mainCss = read('assets/css/main.css');
assert.match(modal, /role="dialog"[^>]*aria-modal="true"[^>]*aria-labelledby="aksrt-auth-modal-title"/);
assert.match(modal, /id="aksrt-auth-modal-title"/);
assert.match(modal, /role="dialog"[^>]*aria-modal="true"[^>]*aria-labelledby="jio-auth-modal-title"/);
assert.match(modal, /id="jio-auth-modal-title"/);
assert.match(client, /focusableModalItems/);
assert.match(client, /lastModalTrigger/);
assert.match(client, /restore\.focus\(\)/);
@@ -253,7 +253,7 @@ test('the social-login area drops the implicit agreement line', () => {
});
test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
const icons = read('inc/helpers/aksrt-icons.php');
const icons = read('inc/helpers/jio-icons.php');
const sprites = read('assets/js/runtime/svg-icon.js');
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so both
@@ -262,17 +262,17 @@ test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
assert.match(icons, /'google'\s*=>\s*'zibsvg-google-color'/);
assert.match(icons, /'github'\s*=>\s*'fa fab fa-github'/);
assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/);
assert.match(sprites, /aksrt_svgs\['gitee'\]/);
assert.match(sprites, /aksrt_svgs\['google-color'\]/);
assert.match(sprites, /jio_svgs\['gitee'\]/);
assert.match(sprites, /jio_svgs\['google-color'\]/);
// Sprite values must reach the shared icon renderer instead of being wrapped
// in a bogus <i class="zibsvg-…">.
assert.match(icons, /strpos\(\$class, 'zibsvg-'\) === 0[\s\S]*?aksrt_icon_markup\(\$class, \$extra_classes\)/);
assert.match(icons, /strpos\(\$class, 'zibsvg-'\) === 0[\s\S]*?jio_icon_markup\(\$class, \$extra_classes\)/);
});
test('the top-nav user popover reproduces the reference quick-login block', () => {
const oauth = read('src/Auth/OAuth.php');
const header = read('inc/helpers/header/aksrt-header-navbar.php');
const links = read('inc/helpers/aksrt-links.php');
const header = read('inc/helpers/header/jio-header-navbar.php');
const links = read('inc/helpers/jio-links.php');
const legacyCss = read('assets/css/frontend/pages/base-1.css');
assert.match(oauth, /social-separator separator muted-3-color em09 mt10/);
@@ -284,9 +284,9 @@ test('the top-nav user popover reproduces the reference quick-login block', () =
assert.match(oauth, /oauth_button_lg/);
// Callers append the block verbatim — no hand-rolled separator or wrapper.
assert.match(header, /render_social_buttons\(__\('快速登录', 'aksrt'\)\)/);
assert.match(header, /render_social_buttons\(__\('快速登录', 'jio'\)\)/);
assert.doesNotMatch(header, /social-separator|social_loginbar/);
assert.match(links, /render_social_buttons\(__\('使用以下方式快速登录', 'aksrt'\)\)/);
assert.match(links, /render_social_buttons\(__\('使用以下方式快速登录', 'jio'\)\)/);
assert.doesNotMatch(links, /social_loginbar/);
// The retired .social-login-divider / .oauth-row / .oauth-btn markup and its
@@ -299,7 +299,7 @@ test('provider URLs stay raw in the data layer and are escaped once per surface'
const oauth = read('src/Auth/OAuth.php');
const shared = read('templates/parts/auth-content.php');
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
const comments = read('inc/helpers/aksrt-comments.php');
const comments = read('inc/helpers/jio-comments.php');
// get_auth_providers_data() hands out raw URLs, exactly like its sibling
// get_frontend_bindings_data() does. esc_url() is idempotent on its own
@@ -9,8 +9,8 @@
* 的 opacity:0 形成隐形遮罩 → 整页所有按钮不可点击(2026-09-17 实测故障)。
*
* 因此 credits.css 必须以 (0,2,0) 特异性压制:
* .modal[data-aksrt-charge-modal] { display: none; }
* .modal[data-aksrt-charge-modal].in { display: flex; }
* .modal[data-jio-charge-modal] { display: none; }
* .modal[data-jio-charge-modal].in { display: flex; }
* 展示态由 credits.js 自包含开关的内联 display:flex 驱动(不依赖
* jQuery/Bootstrap 运行时;block 会破坏 .flex.jc 双轴居中——
* .modal.flex .modal-dialog{margin:0} 在 block 下使对话框顶在左上角),
@@ -30,32 +30,32 @@ const creditsCss = read('assets', 'css', 'routes', 'user-center', 'credits.css')
const balanceRest = read('src', 'Credits', 'BalanceServiceRest.php');
test('弹窗壳为 Zibll refresh_modal 同构静态壳并带 data 钩子', () => {
assert.match(wallet, /class="modal flex jc fade"[^>]*data-aksrt-charge-modal/);
assert.match(wallet, /data-aksrt-charge-form/);
assert.match(wallet, /class="modal flex jc fade"[^>]*data-jio-charge-modal/);
assert.match(wallet, /data-jio-charge-form/);
assert.match(wallet, /data-dismiss="modal"/);
assert.match(wallet, /initiate-pay/);
});
test('弹窗壳静止态必须被 credits.css 显式隐藏(.flex 覆盖回归护栏)', () => {
// (0,2,0) 特异性压制 main.css 的 .flex{display:flex}。
assert.match(creditsCss, /\.modal\[data-aksrt-charge-modal\]\s*\{\s*display:\s*none;/);
assert.match(creditsCss, /\.modal\[data-jio-charge-modal\]\s*\{\s*display:\s*none;/);
// 打开态必须是 flex 而非 block:.modal.flex .modal-dialog{margin:0} 在
// block 下使对话框顶在左上角(无头 Chrome 几何实证),flex 下由壳的
// .flex.jc 双轴居中(Zibll 生产环境即此形态)。
assert.match(creditsCss, /\.modal\[data-aksrt-charge-modal\]\.in\s*\{\s*display:\s*flex;/);
assert.doesNotMatch(creditsCss, /\.modal\[data-aksrt-charge-modal\]\.in\s*\{\s*display:\s*block;/);
assert.match(creditsCss, /\.modal\[data-jio-charge-modal\]\.in\s*\{\s*display:\s*flex;/);
assert.doesNotMatch(creditsCss, /\.modal\[data-jio-charge-modal\]\.in\s*\{\s*display:\s*block;/);
// 购买积分弹窗壳同构(服务端静态输出 .modal.flex.jc.fade),静止态同样
// 必须显式隐藏,否则隐形遮罩拦截整页点击(2026-09-18 实测回归)。
const balance = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
assert.match(balance, /class="modal flex jc fade"[^>]*data-aksrt-points-pay-modal/);
assert.match(creditsCss, /\.modal\[data-aksrt-points-pay-modal\]\s*\{\s*display:\s*none;/);
assert.match(creditsCss, /\.modal\[data-aksrt-points-pay-modal\]\.in\s*\{\s*display:\s*flex;/);
assert.match(balance, /class="modal flex jc fade"[^>]*data-jio-points-pay-modal/);
assert.match(creditsCss, /\.modal\[data-jio-points-pay-modal\]\s*\{\s*display:\s*none;/);
assert.match(creditsCss, /\.modal\[data-jio-points-pay-modal\]\.in\s*\{\s*display:\s*flex;/);
});
test('开关自包含:不依赖 jQuery/Bootstrap modal 运行时', () => {
assert.match(creditsJs, /function openChargeModal/);
assert.match(creditsJs, /function closeChargeModal/);
assert.match(creditsJs, /data-aksrt-charge-open/);
assert.match(creditsJs, /data-jio-charge-open/);
// 动态创建遮罩层(bootstrap.min.css 已有 .modal-backdrop 规则)。
assert.match(creditsJs, /modal-backdrop fade in/);
// 充值弹窗开关不得回退到 jQuery modal 运行时(时序异常时弹窗打不开)。
+31 -31
View File
@@ -44,24 +44,24 @@ test('ban queue merges all records, reports status counts, and clamps pages abov
test('real PHP rendering keeps review forms in templates and completed records read-only', () => {
const { html, filtered_html: filteredHtml } = phpFixture();
assert.equal((html.match(/<dialog id="aksrt-ban-dialog"/g) || []).length, 1);
assert.equal((html.match(/<template id="aksrt-ban-detail-/g) || []).length, 20);
assert.match(html, /<template id="aksrt-ban-detail-report-1">\s*<form method="post"/);
assert.match(html, /<template id="aksrt-ban-detail-appeal-501">\s*<form method="post"/);
const resolved = html.match(/<template id="aksrt-ban-detail-report-2">([\s\S]*?)<\/template>/)?.[1] || '';
assert.equal((html.match(/<dialog id="jio-ban-dialog"/g) || []).length, 1);
assert.equal((html.match(/<template id="jio-ban-detail-/g) || []).length, 20);
assert.match(html, /<template id="jio-ban-detail-report-1">\s*<form method="post"/);
assert.match(html, /<template id="jio-ban-detail-appeal-501">\s*<form method="post"/);
const resolved = html.match(/<template id="jio-ban-detail-report-2">([\s\S]*?)<\/template>/)?.[1] || '';
assert.notEqual(resolved, '');
assert.doesNotMatch(resolved, /<form|data-aksrt-ban-submit/);
assert.doesNotMatch(resolved, /<form|data-jio-ban-submit/);
assert.match(resolved, /该记录已处理/);
for (const [name, value] of Object.entries({
action: 'aksrt_users_update_report', report_id: '9001', filter_type: 'report', filter_status: 'pending',
action: 'jio_users_update_report', report_id: '9001', filter_type: 'report', filter_status: 'pending',
s: 'needle', send_user: '7', orderby: 'updated', order: 'ASC', paged: '1',
})) {
assert.equal(inputValue(filteredHtml, name), value, `review form should carry ${name}`);
}
assert.match(filteredHtml, /name="_wpnonce" value="aksrt_users_update_report_9001-nonce"/);
assert.match(filteredHtml, /name="_wpnonce" value="jio_users_update_report_9001-nonce"/);
assert.match(html, /name="user_id" value="501"/);
assert.match(html, /name="_wpnonce" value="aksrt_users_process_appeal_501-nonce"/);
assert.match(html, /name="_wpnonce" value="jio_users_process_appeal_501-nonce"/);
assert.match(html, /<input type="radio"[^>]*required>/);
assert.doesNotMatch(html, /<input type="radio"[^>]*checked/);
});
@@ -84,19 +84,19 @@ test('PHP templates escape all appeal data and report evidence while preserving
test('report details display readable categories separately from optional content and evidence', () => {
const { html } = phpFixture();
const detail = html.match(/<template id="aksrt-ban-detail-report-1">([\s\S]*?)<\/template>/)?.[1] || '';
const category = detail.match(/<h3>举报分类<\/h3><div class="aksrt-ban-copy">([\s\S]*?)<\/div>/)?.[1];
const content = detail.match(/<h3>举报内容与举证<\/h3><div class="aksrt-ban-copy">([\s\S]*?)<\/div>/)?.[1] || '';
const detail = html.match(/<template id="jio-ban-detail-report-1">([\s\S]*?)<\/template>/)?.[1] || '';
const category = detail.match(/<h3>举报分类<\/h3><div class="jio-ban-copy">([\s\S]*?)<\/div>/)?.[1];
const content = detail.match(/<h3>举报内容与举证<\/h3><div class="jio-ban-copy">([\s\S]*?)<\/div>/)?.[1] || '';
assert.equal(category, '版权投诉');
assert.match(content, /Evidence/);
assert.match(content, /https:\/\/evidence\.example\.test\/proof\.jpg/);
assert.doesNotMatch(content, /版权投诉/);
assert.match(html, /class="aksrt-ban-summary">版权投诉<\/div>/);
assert.match(html, /class="jio-ban-summary">版权投诉<\/div>/);
const categoryOnly = html.match(/<template id="aksrt-ban-detail-report-2">([\s\S]*?)<\/template>/)?.[1] || '';
assert.match(categoryOnly, /<h3>举报分类<\/h3><div class="aksrt-ban-copy">人身攻击<\/div>/);
assert.match(categoryOnly, /<h3>举报内容与举证<\/h3><div class="aksrt-ban-copy">未填写<\/div>/);
assert.match(html, /class="aksrt-ban-summary">人身攻击<\/div>/);
const categoryOnly = html.match(/<template id="jio-ban-detail-report-2">([\s\S]*?)<\/template>/)?.[1] || '';
assert.match(categoryOnly, /<h3>举报分类<\/h3><div class="jio-ban-copy">人身攻击<\/div>/);
assert.match(categoryOnly, /<h3>举报内容与举证<\/h3><div class="jio-ban-copy">未填写<\/div>/);
assert.match(html, /class="jio-ban-summary">人身攻击<\/div>/);
});
test('dedicated ban assets enqueue only on the matching native page', () => {
@@ -104,11 +104,11 @@ test('dedicated ban assets enqueue only on the matching native page', () => {
assert.equal(assets.styles.length, 1);
assert.deepEqual(assets.styles[0].slice(0, 3), [
'aksrt-admin-user-ban', 'https://theme.example.test/assets/css/admin/user-ban.css', ['aksrt-admin-native-ui'],
'jio-admin-user-ban', 'https://theme.example.test/assets/css/admin/user-ban.css', ['jio-admin-native-ui'],
]);
assert.equal(assets.scripts.length, 1);
assert.deepEqual(assets.scripts[0].slice(0, 3), [
'aksrt-admin-user-ban', 'https://theme.example.test/assets/js/admin-user-ban.js', [],
'jio-admin-user-ban', 'https://theme.example.test/assets/js/admin-user-ban.js', [],
]);
assert.equal(assets.scripts[0][4], true);
});
@@ -117,9 +117,9 @@ test('PHP actions reject blank notes and stale versions, and preserve filter rou
const { actions } = phpFixture();
assert.equal(actions.blank_report.message, 'report_failed');
assert.match(actions.blank_report.args.aksrt_ban_error, /必须填写处理说明/);
assert.match(actions.blank_report.args.jio_ban_error, /必须填写处理说明/);
assert.equal(actions.stale_report.message, 'report_failed');
assert.match(actions.stale_report.args.aksrt_ban_error, /已变化或已处理/);
assert.match(actions.stale_report.args.jio_ban_error, /已变化或已处理/);
assert.equal(actions.valid_report.message, 'report_updated');
assert.deepEqual({
type: actions.valid_report.args.type,
@@ -132,7 +132,7 @@ test('PHP actions reject blank notes and stale versions, and preserve filter rou
}, { type: 'report', status: 'pending', s: 'needle', send_user: 7, orderby: 'updated', order: 'ASC', paged: 2 });
assert.equal(actions.blank_appeal.message, 'appeal_failed');
assert.match(actions.blank_appeal.args.aksrt_ban_error, /必须填写拒绝原因/);
assert.match(actions.blank_appeal.args.jio_ban_error, /必须填写拒绝原因/);
assert.equal(actions.valid_appeal.message, 'appeal_rejected');
assert.equal(actions.appeal_status, 'rejected');
assert.equal(actions.appeal_payload.admin_note, 'Missing evidence');
@@ -153,15 +153,15 @@ test('ban admin contract keeps the single-dialog flow and separate native operat
const css = read('assets/css/admin/user-ban.css');
assert.match(users, /use UsersMenusBan;/);
assert.match(users, /admin_post_aksrt_users_update_report/);
assert.match(users, /admin_post_aksrt_users_process_appeal/);
assert.match(users, /admin_post_jio_users_update_report/);
assert.match(users, /admin_post_jio_users_process_appeal/);
assert.doesNotMatch(users, /use UsersMenusTickets;/);
assert.ok(!fs.existsSync(path.join(root, 'src/Admin/Native/UsersMenusTickets.php')));
assert.match(page, /if \(\$hook !== 'users_page_aksrt_user_ban'\) return/);
assert.match(page, /<dialog id="aksrt-ban-dialog"/);
assert.match(dialogs, /<template id="aksrt-ban-detail-/);
assert.match(page, /if \(\$hook !== 'users_page_jio_user_ban'\) return/);
assert.match(page, /<dialog id="jio-ban-dialog"/);
assert.match(dialogs, /<template id="jio-ban-detail-/);
assert.doesNotMatch(page, /update_user_meta|update_status|do_action\(/);
assert.doesNotMatch(page, /data-aksrt-ticket-toggle|data-aksrt-ticket-cancel|data-aksrt-ban-toggle/);
assert.doesNotMatch(page, /data-jio-ticket-toggle|data-jio-ticket-cancel|data-jio-ban-toggle/);
assert.match(query, /BAN_PER_PAGE = 20/);
assert.match(query, /number'\s*=>\s*-1/);
@@ -179,8 +179,8 @@ test('ban admin contract keeps the single-dialog flow and separate native operat
assert.match(dialogs, /esc_html\(\$value !== '' \? \$value : '—'\)/);
assert.match(dialogs, /target="_blank" rel="noopener noreferrer"/);
assert.match(actions, /check_admin_referer\('aksrt_users_update_report_'/);
assert.match(actions, /check_admin_referer\('aksrt_users_process_appeal_'/);
assert.match(actions, /check_admin_referer\('jio_users_update_report_'/);
assert.match(actions, /check_admin_referer\('jio_users_process_appeal_'/);
assert.match(actions, /hash_equals\(\$this->ban_record_version\(\$row\), \$submitted\)/);
assert.match(actions, /\$this->ban_request_is_current\(\$row, 'pending'\)/);
assert.match(actions, /\$decision === 'reject' && \$note === ''/);
@@ -190,5 +190,5 @@ test('ban admin contract keeps the single-dialog flow and separate native operat
assert.match(script, /dialog\.replaceChildren\(\)/);
assert.match(script, /previousOpener\.focus\(\)/);
assert.match(script, /window\.addEventListener\('pageshow'/);
assert.match(css, /html\.aksrt-ban-modal-open \{ overflow: hidden/);
assert.match(css, /html\.jio-ban-modal-open \{ overflow: hidden/);
});
+73 -73
View File
@@ -2,16 +2,16 @@
/**
* Exercise the native ban queue, detail templates, and post handlers with WordPress fixtures.
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Core {
namespace Jio\Core {
final class DBMigrator {
public static function table(string $name): string { return 'wp_aksrt_' . $name; }
public static function table(string $name): string { return 'wp_jio_' . $name; }
}
}
namespace AKSRT\User {
namespace Jio\User {
final class Restriction {
public const TYPE_WARN = 'warn';
public const TYPE_MUTE = 'mute';
@@ -25,27 +25,27 @@ namespace AKSRT\User {
}
final class BanAppeal {
public const META_STATUS = '_aksrt_ban_appeal_status';
public const META_PAYLOAD = '_aksrt_ban_appeal_payload';
public const META_HISTORY = '_aksrt_ban_appeal_history';
public const META_STATUS = '_jio_ban_appeal_status';
public const META_PAYLOAD = '_jio_ban_appeal_payload';
public const META_HISTORY = '_jio_ban_appeal_history';
public static function get_status(int $user_id): string {
return (string) ($GLOBALS['aksrt_test_user_meta'][$user_id][self::META_STATUS] ?? '');
return (string) ($GLOBALS['jio_test_user_meta'][$user_id][self::META_STATUS] ?? '');
}
public static function get_payload(int $user_id): array {
$value = $GLOBALS['aksrt_test_user_meta'][$user_id][self::META_PAYLOAD] ?? [];
$value = $GLOBALS['jio_test_user_meta'][$user_id][self::META_PAYLOAD] ?? [];
return is_array($value) ? $value : [];
}
}
}
namespace AKSRT\Messages {
namespace Jio\Messages {
final class ReportCenter {
private static ?self $instance = null;
public static function get_instance(): self { return self::$instance ??= new self(); }
public function update_status(int $id, string $status, string $note): bool {
$GLOBALS['aksrt_test_reports'][$id]['status'] = $status;
$GLOBALS['aksrt_test_reports'][$id]['updated_at'] = '2026-10-30 13:00:00';
$GLOBALS['aksrt_test_report_actions'][] = [$id, $status, $note];
$GLOBALS['jio_test_reports'][$id]['status'] = $status;
$GLOBALS['jio_test_reports'][$id]['updated_at'] = '2026-10-30 13:00:00';
$GLOBALS['jio_test_report_actions'][] = [$id, $status, $note];
return true;
}
}
@@ -54,15 +54,15 @@ namespace AKSRT\Messages {
private static ?self $instance = null;
public static function get_instance(): self { return self::$instance ??= new self(); }
public function send(int $user_id, string $type, string $message, array $args = []): void {
$GLOBALS['aksrt_test_messages'][] = [$user_id, $type, $message, $args];
$GLOBALS['jio_test_messages'][] = [$user_id, $type, $message, $args];
}
}
}
namespace AKSRT\Admin\Native {
namespace Jio\Admin\Native {
final class AdminListPagination {
public static function render(string $slug, int $total, int $page, int $per_page, array $filters = [], string $parent = ''): void {
$GLOBALS['aksrt_test_pagination'][] = [$slug, $total, $page, $per_page, $filters, $parent];
$GLOBALS['jio_test_pagination'][] = [$slug, $total, $page, $per_page, $filters, $parent];
echo '<nav data-test-pagination="' . esc_attr((string) $total) . ':' . esc_attr((string) $page) . '"></nav>';
}
}
@@ -70,8 +70,8 @@ namespace AKSRT\Admin\Native {
namespace {
define('ABSPATH', dirname(__DIR__, 2) . DIRECTORY_SEPARATOR);
define('AKSRT_DIR', dirname(__DIR__, 2) . DIRECTORY_SEPARATOR);
define('AKSRT_URI', 'https://theme.example.test');
define('JIO_DIR', dirname(__DIR__, 2) . DIRECTORY_SEPARATOR);
define('JIO_URI', 'https://theme.example.test');
define('ARRAY_A', 'ARRAY_A');
class WP_User {
@@ -98,13 +98,13 @@ namespace {
if (str_contains($query, 'notes_json')) {
return array_map(static fn (int $id): array => [
'id' => $id,
'notes_json' => $GLOBALS['aksrt_test_reports'][$id]['notes_json'] ?? '[]',
'notes_json' => $GLOBALS['jio_test_reports'][$id]['notes_json'] ?? '[]',
], $this->args);
}
if (str_contains($query, 'aksrt_user_restriction')) {
return $GLOBALS['aksrt_test_restrictions'];
if (str_contains($query, 'jio_user_restriction')) {
return $GLOBALS['jio_test_restrictions'];
}
$reports = array_values($GLOBALS['aksrt_test_reports']);
$reports = array_values($GLOBALS['jio_test_reports']);
$search = '';
$send_user = 0;
foreach ($this->args as $arg) {
@@ -119,14 +119,14 @@ namespace {
}
public function get_row(string $query, $output = null) {
$id = (int) ($this->args[0] ?? 0);
return $GLOBALS['aksrt_test_reports'][$id] ?? null;
return $GLOBALS['jio_test_reports'][$id] ?? null;
}
}
$GLOBALS['wpdb'] = new TestWpdb();
$GLOBALS['aksrt_test_reports'] = [];
$GLOBALS['aksrt_test_user_meta'] = [];
$GLOBALS['aksrt_test_restrictions'] = [[
$GLOBALS['jio_test_reports'] = [];
$GLOBALS['jio_test_user_meta'] = [];
$GLOBALS['jio_test_restrictions'] = [[
'user_id' => 7,
'type' => 'ban',
'scope' => 'site',
@@ -134,12 +134,12 @@ namespace {
'created_at' => '2026-10-01 08:00:00',
'expires_at' => null,
]];
$GLOBALS['aksrt_test_assets'] = ['styles' => [], 'scripts' => []];
$GLOBALS['aksrt_test_pagination'] = [];
$GLOBALS['aksrt_test_report_actions'] = [];
$GLOBALS['aksrt_test_messages'] = [];
$GLOBALS['aksrt_test_nonce_checks'] = [];
$GLOBALS['aksrt_test_capability'] = true;
$GLOBALS['jio_test_assets'] = ['styles' => [], 'scripts' => []];
$GLOBALS['jio_test_pagination'] = [];
$GLOBALS['jio_test_report_actions'] = [];
$GLOBALS['jio_test_messages'] = [];
$GLOBALS['jio_test_nonce_checks'] = [];
$GLOBALS['jio_test_capability'] = true;
function __($text, $domain = null): string { return (string) $text; }
function esc_html__($text, $domain = null): string { return esc_html((string) $text); }
@@ -174,28 +174,28 @@ namespace {
$prefix .= $parts['path'] ?? '';
return $prefix . '?' . http_build_query($query);
}
function current_user_can(string $capability): bool { return $capability === 'manage_options' && $GLOBALS['aksrt_test_capability']; }
function current_user_can(string $capability): bool { return $capability === 'manage_options' && $GLOBALS['jio_test_capability']; }
function wp_die($message): void { throw new \RuntimeException((string) $message); }
function wp_nonce_field(string $action): void { echo '<input type="hidden" name="_wpnonce" value="' . esc_attr($action . '-nonce') . '">'; }
function check_admin_referer(string $action): void {
$GLOBALS['aksrt_test_nonce_checks'][] = $action;
$GLOBALS['jio_test_nonce_checks'][] = $action;
if (($_POST['_wpnonce'] ?? '') !== $action . '-nonce') throw new \RuntimeException('nonce mismatch');
}
function wp_enqueue_style(string $handle, string $src, array $deps = [], string $version = ''): void {
$GLOBALS['aksrt_test_assets']['styles'][] = [$handle, $src, $deps, $version];
$GLOBALS['jio_test_assets']['styles'][] = [$handle, $src, $deps, $version];
}
function wp_enqueue_script(string $handle, string $src, array $deps = [], string $version = '', bool $footer = false): void {
$GLOBALS['aksrt_test_assets']['scripts'][] = [$handle, $src, $deps, $version, $footer];
$GLOBALS['jio_test_assets']['scripts'][] = [$handle, $src, $deps, $version, $footer];
}
function get_userdata(int $user_id) { return $user_id > 0 ? new WP_User($user_id, $user_id === 7 ? 'Report <script>alert(1)</script>' : 'User ' . $user_id) : false; }
function get_current_user_id(): int { return 999; }
function get_user_meta(int $user_id, string $key, bool $single = false) { return $GLOBALS['aksrt_test_user_meta'][$user_id][$key] ?? ''; }
function update_user_meta(int $user_id, string $key, $value): bool { $GLOBALS['aksrt_test_user_meta'][$user_id][$key] = $value; return true; }
function delete_user_meta(int $user_id, string $key): bool { unset($GLOBALS['aksrt_test_user_meta'][$user_id][$key]); return true; }
function get_user_meta(int $user_id, string $key, bool $single = false) { return $GLOBALS['jio_test_user_meta'][$user_id][$key] ?? ''; }
function update_user_meta(int $user_id, string $key, $value): bool { $GLOBALS['jio_test_user_meta'][$user_id][$key] = $value; return true; }
function delete_user_meta(int $user_id, string $key): bool { unset($GLOBALS['jio_test_user_meta'][$user_id][$key]); return true; }
function get_users(array $args = []): array {
$ids = array_keys($GLOBALS['aksrt_test_user_meta']);
$ids = array_keys($GLOBALS['jio_test_user_meta']);
if (isset($args['include'])) $ids = array_values(array_intersect($ids, $args['include']));
$ids = array_values(array_filter($ids, static fn (int $id): bool => \AKSRT\User\BanAppeal::get_status($id) !== ''));
$ids = array_values(array_filter($ids, static fn (int $id): bool => \Jio\User\BanAppeal::get_status($id) !== ''));
return $args['fields'] === 'ID' ? $ids : array_map('get_userdata', $ids);
}
function update_meta_cache(string $type, array $ids): void { unset($type, $ids); }
@@ -213,7 +213,7 @@ namespace {
function get_comment(int $id) { return false; }
function get_comment_link(int $id): string { return 'https://content.example.test/comment/' . $id; }
function get_author_posts_url(int $user_id): string { return 'https://content.example.test/author/' . $user_id; }
function aksrt_get_user_home_url(int $user_id): string { return 'https://content.example.test/user/' . $user_id; }
function jio_get_user_home_url(int $user_id): string { return 'https://content.example.test/user/' . $user_id; }
function do_action(string $hook, ...$args): void { unset($hook, $args); }
function fixture_report(int $id, int $reporter, string $status, string $time, string $reason = ''): array {
@@ -238,17 +238,17 @@ namespace {
? 'Evidence <script>alert(1)</script> https://evidence.example.test/proof.jpg javascript:alert(2)'
: 'report reason ' . $id;
$time = $id === 1 ? '2026-10-30 12:00:00' : ($id === 2 ? '2026-10-29 11:00:00' : $time);
$GLOBALS['aksrt_test_reports'][$id] = fixture_report($id, $id === 1 ? 7 : 20 + $id, $status, $time, $reason);
$GLOBALS['jio_test_reports'][$id] = fixture_report($id, $id === 1 ? 7 : 20 + $id, $status, $time, $reason);
}
$GLOBALS['aksrt_test_reports'][1]['category'] = '版权投诉';
$GLOBALS['aksrt_test_reports'][2]['category'] = '人身攻击';
$GLOBALS['aksrt_test_reports'][2]['reason'] = '';
$GLOBALS['aksrt_test_reports'][9001] = fixture_report(9001, 7, 'pending', '2026-09-01 09:00:00', 'needle action report');
$GLOBALS['aksrt_test_reports'][9002] = fixture_report(9002, 7, 'pending', '2026-09-02 09:00:00', 'needle stale report');
$GLOBALS['jio_test_reports'][1]['category'] = '版权投诉';
$GLOBALS['jio_test_reports'][2]['category'] = '人身攻击';
$GLOBALS['jio_test_reports'][2]['reason'] = '';
$GLOBALS['jio_test_reports'][9001] = fixture_report(9001, 7, 'pending', '2026-09-01 09:00:00', 'needle action report');
$GLOBALS['jio_test_reports'][9002] = fixture_report(9002, 7, 'pending', '2026-09-02 09:00:00', 'needle stale report');
function fixture_appeal(int $user_id, string $status, string $time, array $payload = []): void {
$GLOBALS['aksrt_test_user_meta'][$user_id][\AKSRT\User\BanAppeal::META_STATUS] = $status;
$GLOBALS['aksrt_test_user_meta'][$user_id][\AKSRT\User\BanAppeal::META_PAYLOAD] = array_merge([
$GLOBALS['jio_test_user_meta'][$user_id][\Jio\User\BanAppeal::META_STATUS] = $status;
$GLOBALS['jio_test_user_meta'][$user_id][\Jio\User\BanAppeal::META_PAYLOAD] = array_merge([
'time' => $time,
'reason_key' => 'ban reason',
'reason' => 'appeal explanation',
@@ -270,14 +270,14 @@ namespace {
fixture_appeal(7001, 'pending', '2026-09-03 09:00:00', ['reason' => 'pending appeal action']);
fixture_appeal(7002, 'pending', '2026-09-04 09:00:00', ['reason' => 'pending appeal approve']);
require_once AKSRT_DIR . 'src/Admin/Native/NativeUi.php';
require_once AKSRT_DIR . 'src/Admin/Native/UsersMenusBanQuery.php';
require_once AKSRT_DIR . 'src/Admin/Native/UsersMenusBanDialogs.php';
require_once AKSRT_DIR . 'src/Admin/Native/UsersMenusBanActions.php';
require_once AKSRT_DIR . 'src/Admin/Native/UsersMenusBan.php';
require_once JIO_DIR . 'src/Admin/Native/NativeUi.php';
require_once JIO_DIR . 'src/Admin/Native/UsersMenusBanQuery.php';
require_once JIO_DIR . 'src/Admin/Native/UsersMenusBanDialogs.php';
require_once JIO_DIR . 'src/Admin/Native/UsersMenusBanActions.php';
require_once JIO_DIR . 'src/Admin/Native/UsersMenusBan.php';
final class BanFixtureMenu {
use \AKSRT\Admin\Native\UsersMenusBan;
use \Jio\Admin\Native\UsersMenusBan;
public const CAPABILITY = 'manage_options';
public function ensure_cap(): void {}
public function flash(): void {}
@@ -324,15 +324,15 @@ namespace {
'type' => 'report', 'status' => '', 's' => 'needle', 'send_user' => '7',
'orderby' => 'updated', 'order' => 'ASC',
]);
$html = render_ban_page(['page' => 'aksrt_user_ban', 'status' => '', 'orderby' => 'time', 'order' => 'DESC']);
$html = render_ban_page(['page' => 'jio_user_ban', 'status' => '', 'orderby' => 'time', 'order' => 'DESC']);
$filteredHtml = render_ban_page([
'page' => 'aksrt_user_ban', 'type' => 'report', 'status' => 'pending', 's' => 'needle',
'page' => 'jio_user_ban', 'type' => 'report', 'status' => 'pending', 's' => 'needle',
'send_user' => '7', 'orderby' => 'updated', 'order' => 'ASC', 'paged' => '1',
]);
$GLOBALS['aksrt_test_assets'] = ['styles' => [], 'scripts' => []];
$GLOBALS['jio_test_assets'] = ['styles' => [], 'scripts' => []];
$assetMenu = new BanFixtureMenu();
$assetMenu->enqueue_ban_assets('users_page_aksrt_user_ban');
$assetMenu->enqueue_ban_assets('users_page_jio_user_ban');
$assetMenu->enqueue_ban_assets('users_page_other');
$menu = new BanFixtureMenu();
@@ -343,9 +343,9 @@ namespace {
'filter_type' => 'report', 'filter_status' => 'pending', 's' => 'needle', 'send_user' => '7',
'orderby' => 'updated', 'order' => 'ASC', 'paged' => '2',
];
$blankReport = call_action($menu, 'handle_update_report', 'aksrt_users_update_report_9001', array_merge($reportBase, ['new_status' => 'dismissed', 'note' => ' ']));
$staleReport = call_action($menu, 'handle_update_report', 'aksrt_users_update_report_9002', array_merge($reportBase, ['report_id' => '9002', 'record_version' => 'stale-version']));
$validReport = call_action($menu, 'handle_update_report', 'aksrt_users_update_report_9001', $reportBase);
$blankReport = call_action($menu, 'handle_update_report', 'jio_users_update_report_9001', array_merge($reportBase, ['new_status' => 'dismissed', 'note' => ' ']));
$staleReport = call_action($menu, 'handle_update_report', 'jio_users_update_report_9002', array_merge($reportBase, ['report_id' => '9002', 'record_version' => 'stale-version']));
$validReport = call_action($menu, 'handle_update_report', 'jio_users_update_report_9001', $reportBase);
$appealRow = $menu->query_record('appeal', 7001);
$appealVersion = $menu->version($appealRow);
@@ -354,15 +354,15 @@ namespace {
'filter_type' => 'appeal', 'filter_status' => 'pending', 's' => 'needle', 'send_user' => '7001',
'orderby' => 'updated', 'order' => 'ASC', 'paged' => '3',
];
$blankAppeal = call_action($menu, 'handle_process_appeal', 'aksrt_users_process_appeal_7001', array_merge($appealBase, ['note' => ' ']));
$validAppeal = call_action($menu, 'handle_process_appeal', 'aksrt_users_process_appeal_7001', $appealBase);
$blankAppeal = call_action($menu, 'handle_process_appeal', 'jio_users_process_appeal_7001', array_merge($appealBase, ['note' => ' ']));
$validAppeal = call_action($menu, 'handle_process_appeal', 'jio_users_process_appeal_7001', $appealBase);
$approveRow = $menu->query_record('appeal', 7002);
$approve = call_action($menu, 'handle_process_appeal', 'aksrt_users_process_appeal_7002', [
$approve = call_action($menu, 'handle_process_appeal', 'jio_users_process_appeal_7002', [
'user_id' => '7002', 'decision' => 'approve', 'note' => '', 'record_version' => $menu->version($approveRow),
]);
$filtersHtml = render_ban_page(['page' => 'aksrt_user_ban', 'type' => 'report', 'status' => 'pending', 's' => 'needle', 'send_user' => '7', 'orderby' => 'updated', 'order' => 'ASC']);
$filtersHtml = render_ban_page(['page' => 'jio_user_ban', 'type' => 'report', 'status' => 'pending', 's' => 'needle', 'send_user' => '7', 'orderby' => 'updated', 'order' => 'ASC']);
echo json_encode([
'queue' => [
@@ -372,15 +372,15 @@ namespace {
'filters' => $filters,
'html' => $html,
'filtered_html' => $filteredHtml,
'assets' => $GLOBALS['aksrt_test_assets'],
'assets' => $GLOBALS['jio_test_assets'],
'actions' => [
'blank_report' => $blankReport, 'stale_report' => $staleReport, 'valid_report' => $validReport,
'blank_appeal' => $blankAppeal, 'valid_appeal' => $validAppeal, 'approve_appeal' => $approve,
'report_calls' => $GLOBALS['aksrt_test_report_actions'], 'messages' => $GLOBALS['aksrt_test_messages'],
'appeal_status' => \AKSRT\User\BanAppeal::get_status(7001),
'appeal_payload' => \AKSRT\User\BanAppeal::get_payload(7001),
'approved_status' => \AKSRT\User\BanAppeal::get_status(7002),
'restrictions_lifted' => \AKSRT\User\Restriction::$lifted,
'report_calls' => $GLOBALS['jio_test_report_actions'], 'messages' => $GLOBALS['jio_test_messages'],
'appeal_status' => \Jio\User\BanAppeal::get_status(7001),
'appeal_payload' => \Jio\User\BanAppeal::get_payload(7001),
'approved_status' => \Jio\User\BanAppeal::get_status(7002),
'restrictions_lifted' => \Jio\User\Restriction::$lifted,
],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
+35 -35
View File
@@ -65,7 +65,7 @@ function runtime({ completed = false } = {}) {
}
map(selector, elements) { this.queries.set(selector, elements); return this; }
closest(selector) {
if (selector === '[data-aksrt-dialog-close]' && this.attrs['data-aksrt-dialog-close'] !== undefined) return this;
if (selector === '[data-jio-dialog-close]' && this.attrs['data-jio-dialog-close'] !== undefined) return this;
return null;
}
focus() { doc.activeElement = this; }
@@ -76,11 +76,11 @@ function runtime({ completed = false } = {}) {
function fragment({ readOnly = false, initiallyChecked = false } = {}) {
const part = new Element();
const title = new Element({ id: 'aksrt-ban-dialog-title', autofocus: '' });
const closeIcon = new Element({ 'data-aksrt-dialog-close': '' });
const closeFooter = new Element({ 'data-aksrt-dialog-close': '' });
part.map('#aksrt-ban-dialog-title', title)
.map('[data-aksrt-dialog-close]', [closeIcon, closeFooter]);
const title = new Element({ id: 'jio-ban-dialog-title', autofocus: '' });
const closeIcon = new Element({ 'data-jio-dialog-close': '' });
const closeFooter = new Element({ 'data-jio-dialog-close': '' });
part.map('#jio-ban-dialog-title', title)
.map('[data-jio-dialog-close]', [closeIcon, closeFooter]);
const controls = [closeIcon, closeFooter];
let form = null;
let radios = [];
@@ -92,31 +92,31 @@ function runtime({ completed = false } = {}) {
new Element({ type: 'radio', value: 'reject', required: '', checked: initiallyChecked }),
];
note = new Element({ name: 'note', value: 'old note', 'data-required-error': 'Please enter a nonblank rejection reason' });
const label = new Element({ 'data-aksrt-ban-note-label': '', 'data-optional': 'Optional note', 'data-required': 'Reject reason required' });
submit = new Element({ 'data-aksrt-ban-submit': '', 'data-idle-label': 'Choose decision', 'data-approve-label': 'Confirm approval', 'data-reject-label': 'Confirm rejection', 'data-busy-label': 'Submitting' });
const label = new Element({ 'data-jio-ban-note-label': '', 'data-optional': 'Optional note', 'data-required': 'Reject reason required' });
submit = new Element({ 'data-jio-ban-submit': '', 'data-idle-label': 'Choose decision', 'data-approve-label': 'Confirm approval', 'data-reject-label': 'Confirm rejection', 'data-busy-label': 'Submitting' });
controls.push(submit);
form = new Element();
form.dataset = {};
form.elements = { radios, note };
form.map('input[type="radio"]', radios)
.map('textarea[name="note"]', note)
.map('[data-aksrt-ban-note-label]', label)
.map('[data-aksrt-ban-submit]', submit);
.map('[data-jio-ban-note-label]', label)
.map('[data-jio-ban-submit]', submit);
form.reportValidity = () => {
const hasRequiredRadio = radios.some((radio) => radio.required);
const radioValid = !hasRequiredRadio || radios.some((radio) => radio.checked);
return radioValid && !note.validity && (!note.required || note.value.trim() !== '');
};
part.map('form[data-aksrt-ban-review]', form);
part.map('form[data-jio-ban-review]', form);
}
part.map('[data-aksrt-ban-submit], [data-aksrt-dialog-close]', controls);
part.map('[data-jio-ban-submit], [data-jio-dialog-close]', controls);
return { part, form, radios, note, submit, title, closeIcon, closeFooter };
}
const root = new Element();
const window = new Element();
doc.documentElement = new Element();
doc.querySelector = (selector) => selector === '.aksrt-ban-page' ? root : null;
doc.querySelector = (selector) => selector === '.jio-ban-page' ? root : null;
doc.getElementById = (id) => ids.get(id) || null;
const dialog = new Element();
const originalDialogQuery = dialog.querySelector.bind(dialog);
@@ -125,7 +125,7 @@ function runtime({ completed = false } = {}) {
dialog.querySelectorAll = (selector) => dialog.currentContent ? dialog.currentContent.querySelectorAll(selector) : originalDialogQueryAll(selector);
dialog.showModal = () => {
dialog.open = true;
const title = dialog.querySelector('#aksrt-ban-dialog-title');
const title = dialog.querySelector('#jio-ban-dialog-title');
if (title) title.focus();
};
dialog.close = () => {
@@ -133,20 +133,20 @@ function runtime({ completed = false } = {}) {
dialog.open = false;
dialog.fire('close');
};
ids.set('aksrt-ban-dialog', dialog);
ids.set('jio-ban-dialog', dialog);
const opener = new Element({ 'data-aksrt-ban-view': 'report-1' });
const readonlyOpener = new Element({ 'data-aksrt-ban-view': 'report-2' });
const opener = new Element({ 'data-jio-ban-view': 'report-1' });
const readonlyOpener = new Element({ 'data-jio-ban-view': 'report-2' });
const templates = new Map();
templates.set('aksrt-ban-detail-report-1', {
templates.set('jio-ban-detail-report-1', {
content: { cloneNode: () => fragment().part },
});
templates.set('aksrt-ban-detail-report-2', {
templates.set('jio-ban-detail-report-2', {
content: { cloneNode: () => fragment({ readOnly: true }).part },
});
ids.set('aksrt-ban-detail-report-1', templates.get('aksrt-ban-detail-report-1'));
ids.set('aksrt-ban-detail-report-2', templates.get('aksrt-ban-detail-report-2'));
root.map('[data-aksrt-ban-view]', [opener, readonlyOpener]);
ids.set('jio-ban-detail-report-1', templates.get('jio-ban-detail-report-1'));
ids.set('jio-ban-detail-report-2', templates.get('jio-ban-detail-report-2'));
root.map('[data-jio-ban-view]', [opener, readonlyOpener]);
vm.runInNewContext(fs.readFileSync(path.join(rootDir, 'assets/js/admin-user-ban.js'), 'utf8'), { document: doc, window });
return { doc, window, root, dialog, opener, readonlyOpener };
@@ -155,15 +155,15 @@ function runtime({ completed = false } = {}) {
function openReview(runtimeState, button = runtimeState.opener) {
button.fire('click');
const frame = runtimeState.dialog.currentContent;
const form = frame.querySelector('form[data-aksrt-ban-review]');
const form = frame.querySelector('form[data-jio-ban-review]');
return {
frame,
form,
radios: form.querySelectorAll('input[type="radio"]'),
note: form.querySelector('textarea[name="note"]'),
label: form.querySelector('[data-aksrt-ban-note-label]'),
submit: form.querySelector('[data-aksrt-ban-submit]'),
closeButtons: frame.querySelectorAll('[data-aksrt-ban-submit], [data-aksrt-dialog-close]').filter((button) => button.attrs['data-aksrt-dialog-close'] !== undefined),
label: form.querySelector('[data-jio-ban-note-label]'),
submit: form.querySelector('[data-jio-ban-submit]'),
closeButtons: frame.querySelectorAll('[data-jio-ban-submit], [data-jio-dialog-close]').filter((button) => button.attrs['data-jio-dialog-close'] !== undefined),
};
}
@@ -172,12 +172,12 @@ test('opening review clears choices, focuses the dialog, and locks page scrollin
const review = openReview(r);
assert.equal(r.dialog.open, true);
assert.equal(r.doc.activeElement, review.frame.querySelector('#aksrt-ban-dialog-title'));
assert.equal(r.doc.activeElement, review.frame.querySelector('#jio-ban-dialog-title'));
assert.deepEqual(review.radios.map((radio) => radio.checked), [false, false]);
assert.equal(review.note.value, '');
assert.equal(review.submit.disabled, true);
assert.equal(review.label.textContent, 'Optional note');
assert.equal(r.doc.documentElement.classList.contains('aksrt-ban-modal-open'), true);
assert.equal(r.doc.documentElement.classList.contains('jio-ban-modal-open'), true);
});
test('reject decisions require a nonblank trimmed note before submission', () => {
@@ -225,12 +225,12 @@ test('submission blocks duplicate posts and closing until pageshow restores stat
assert.equal(review.form.getAttribute('aria-busy'), null);
assert.equal(review.submit.disabled, false);
assert.ok(review.closeButtons.every((button) => !button.disabled));
assert.equal(r.doc.documentElement.classList.contains('aksrt-ban-modal-open'), true);
assert.equal(r.doc.documentElement.classList.contains('jio-ban-modal-open'), true);
r.dialog.fire('click', { target: review.closeButtons[0] });
assert.equal(r.dialog.open, false);
assert.equal(r.dialog.currentContent, null);
assert.equal(r.doc.documentElement.classList.contains('aksrt-ban-modal-open'), false);
assert.equal(r.doc.documentElement.classList.contains('jio-ban-modal-open'), false);
assert.equal(r.doc.activeElement, r.opener);
});
@@ -239,10 +239,10 @@ test('completed records open read-only without a form or submit controls', () =>
r.readonlyOpener.fire('click');
assert.equal(r.dialog.open, true);
assert.equal(r.dialog.querySelector('form[data-aksrt-ban-review]'), null);
assert.equal(r.dialog.querySelector('[data-aksrt-ban-submit]'), null);
assert.ok(r.dialog.querySelector('[data-aksrt-dialog-close]'));
r.dialog.fire('click', { target: r.dialog.querySelector('[data-aksrt-dialog-close]') });
assert.equal(r.dialog.querySelector('form[data-jio-ban-review]'), null);
assert.equal(r.dialog.querySelector('[data-jio-ban-submit]'), null);
assert.ok(r.dialog.querySelector('[data-jio-dialog-close]'));
r.dialog.fire('click', { target: r.dialog.querySelector('[data-jio-dialog-close]') });
assert.equal(r.dialog.open, false);
assert.equal(r.doc.activeElement, r.readonlyOpener);
});
@@ -256,5 +256,5 @@ test('backdrop closes only when the pointer starts and ends outside the dialog',
r.dialog.fire('pointerdown', { clientX: 0, clientY: 0 });
r.dialog.fire('click', { clientX: 0, clientY: 0 });
assert.equal(r.dialog.open, false);
assert.equal(r.doc.documentElement.classList.contains('aksrt-ban-modal-open'), false);
assert.equal(r.doc.documentElement.classList.contains('jio-ban-modal-open'), false);
});
+29 -29
View File
@@ -19,10 +19,10 @@ set_error_handler(static function (int $severity, string $message, string $file,
throw new ErrorException($message, 0, $severity, $file, $line);
});
$GLOBALS['aksrt_test_state'] = [];
$GLOBALS['jio_test_state'] = [];
function aksrt_test_reset_state(): void {
$GLOBALS['aksrt_test_state'] = [
function jio_test_reset_state(): void {
$GLOBALS['jio_test_state'] = [
'options' => [],
'sidebars' => [],
'routes' => [],
@@ -37,7 +37,7 @@ function aksrt_test_reset_state(): void {
$GLOBALS['wp_registered_sidebars'] = [];
}
aksrt_test_reset_state();
jio_test_reset_state();
class WP_Error {
public function __construct(private string $code = '', private string $message = '', private array $data = []) {}
@@ -81,7 +81,7 @@ class WP_Query {
public int $max_num_pages = 1;
public int $found_posts = 0;
public function __construct(array $args = []) {
$GLOBALS['aksrt_test_state']['query_args'][] = $args;
$GLOBALS['jio_test_state']['query_args'][] = $args;
}
public function have_posts(): bool { return false; }
public function the_post(): void {}
@@ -147,40 +147,40 @@ function wp_trim_words($text, int $count = 55): string {
return implode(' ', array_slice($words, 0, $count));
}
function get_option(string $name, $default = false) { return $GLOBALS['aksrt_test_state']['options'][$name] ?? $default; }
function update_option(string $name, $value): bool { $GLOBALS['aksrt_test_state']['options'][$name] = $value; return true; }
function wp_get_sidebars_widgets(): array { return $GLOBALS['aksrt_test_state']['sidebars']; }
function wp_set_sidebars_widgets(array $sidebars): void { $GLOBALS['aksrt_test_state']['sidebars'] = $sidebars; }
function get_option(string $name, $default = false) { return $GLOBALS['jio_test_state']['options'][$name] ?? $default; }
function update_option(string $name, $value): bool { $GLOBALS['jio_test_state']['options'][$name] = $value; return true; }
function wp_get_sidebars_widgets(): array { return $GLOBALS['jio_test_state']['sidebars']; }
function wp_set_sidebars_widgets(array $sidebars): void { $GLOBALS['jio_test_state']['sidebars'] = $sidebars; }
function register_rest_route(string $namespace, string $route, array $args): bool {
$GLOBALS['aksrt_test_state']['routes'][$namespace . $route] = $args;
$GLOBALS['jio_test_state']['routes'][$namespace . $route] = $args;
return true;
}
function rest_ensure_response($data): WP_REST_Response { return $data instanceof WP_REST_Response ? $data : new WP_REST_Response($data); }
function is_wp_error($value): bool { return $value instanceof WP_Error; }
function is_singular(): bool { return (bool) $GLOBALS['aksrt_test_state']['singular']; }
function get_queried_object_id(): int { return (int) $GLOBALS['aksrt_test_state']['current_id']; }
function is_singular(): bool { return (bool) $GLOBALS['jio_test_state']['singular']; }
function get_queried_object_id(): int { return (int) $GLOBALS['jio_test_state']['current_id']; }
function get_post($post = null) {
if ($post instanceof WP_Post) return $post;
return $GLOBALS['aksrt_test_state']['current_post'];
return $GLOBALS['jio_test_state']['current_post'];
}
function aksrt_get_widget_null(string $message): string { return '<div class="aksrt-widget__empty" role="status">' . esc_html($message) . '</div>'; }
function aksrt_get_pay_meta($post = null): array { unset($post); return $GLOBALS['aksrt_test_state']['pay_meta']; }
function aksrt_has_paid_access($post = null): bool { unset($post); return (bool) $GLOBALS['aksrt_test_state']['paid_access']; }
function aksrt_get_paywall_html(WP_Post $post, array|bool $options = false): string {
function jio_get_widget_null(string $message): string { return '<div class="jio-widget__empty" role="status">' . esc_html($message) . '</div>'; }
function jio_get_pay_meta($post = null): array { unset($post); return $GLOBALS['jio_test_state']['pay_meta']; }
function jio_has_paid_access($post = null): bool { unset($post); return (bool) $GLOBALS['jio_test_state']['paid_access']; }
function jio_get_paywall_html(WP_Post $post, array|bool $options = false): string {
unset($post, $options);
$meta = $GLOBALS['aksrt_test_state']['pay_meta'];
$meta = $GLOBALS['jio_test_state']['pay_meta'];
if ((string) ($meta['pay_type'] ?? 'no') === 'no') {
return '';
}
if ((bool) $GLOBALS['aksrt_test_state']['paid_access']) {
if ((bool) $GLOBALS['jio_test_state']['paid_access']) {
return '<div data-test-paywall="unlocked">内容已解锁</div>';
}
$state = $GLOBALS['aksrt_test_state']['logged_in'] ? 'member-locked' : 'guest-locked';
$state = $GLOBALS['jio_test_state']['logged_in'] ? 'member-locked' : 'guest-locked';
return '<div data-test-paywall="' . $state . '">PAYWALL</div>';
}
function aksrt_get_pay_attributes_html($post, string $class): string { unset($post); return '<ul class="' . esc_attr($class) . '"></ul>'; }
function aksrt_get_demo_link_html($post, string $class): string { unset($post); return '<a class="' . esc_attr($class) . '">DEMO</a>'; }
function is_user_logged_in(): bool { return (bool) $GLOBALS['aksrt_test_state']['logged_in']; }
function jio_get_pay_attributes_html($post, string $class): string { unset($post); return '<ul class="' . esc_attr($class) . '"></ul>'; }
function jio_get_demo_link_html($post, string $class): string { unset($post); return '<a class="' . esc_attr($class) . '">DEMO</a>'; }
function is_user_logged_in(): bool { return (bool) $GLOBALS['jio_test_state']['logged_in']; }
function wp_parse_args($args, array $defaults = []): array {
return array_merge($defaults, is_array($args) ? $args : []);
}
@@ -191,20 +191,20 @@ function wp_kses(string $value, array $allowed_html = [], array $allowed_protoco
// 桩环境信任来源:生产侧的 wp_kses 白名单允许 div/badge 等组合卡标记。
return (string) $value;
}
function aksrt_is_lazy(string $key, bool $default = false): bool { unset($key, $default); return true; }
function aksrt_get_lazy_thumb(string $size = ''): string {
function jio_is_lazy(string $key, bool $default = false): bool { unset($key, $default); return true; }
function jio_get_lazy_thumb(string $size = ''): string {
return 'https://example.test/thumbnail' . ($size !== '' ? '-' . sanitize_key($size) : '') . '.svg';
}
function aksrt_get_lazy_attr(string $key, string $src, string $class = '', string $lazy_src = ''): string {
function jio_get_lazy_attr(string $key, string $src, string $class = '', string $lazy_src = ''): string {
unset($key);
$class = trim($class);
if ($src === '') {
return ' class="' . esc_attr($class) . '" src=""';
}
$lazy_src = $lazy_src !== '' ? $lazy_src : aksrt_get_lazy_thumb();
$lazy_src = $lazy_src !== '' ? $lazy_src : jio_get_lazy_thumb();
return ' class="lazyload ' . esc_attr($class) . '" src="' . esc_url($lazy_src) . '" data-src="' . esc_url($src) . '"';
}
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-icons.php';
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-graphic-card.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-icons.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-graphic-card.php';
require dirname(__DIR__, 2) . '/vendor/autoload.php';
@@ -12,11 +12,11 @@ test('comment REST admission is request-local and preprocess keeps native captch
const controller = read('src/Core/CommentActions.php');
const moderation = read('src/Auth/Moderation.php');
assert.match(controller, /aksrt_require_captcha\('comment', \$params\)/);
assert.match(controller, /jio_require_captcha\('comment', \$params\)/);
assert.match(controller, /begin_captcha_verified_comment_request\(\)[\s\S]*?try \{[\s\S]*?wp_new_comment[\s\S]*?finally \{[\s\S]*?end_captcha_verified_comment_request\(\)/);
assert.match(moderation, /private \$captcha_verified_comment_request_depth = 0/);
assert.match(moderation, /if \(\$this->captcha_verified_comment_request_depth === 0\) \{/);
assert.doesNotMatch(controller + moderation, /function_exists\('aksrt_require_captcha'\)/);
assert.doesNotMatch(controller + moderation, /function_exists\('jio_require_captcha'\)/);
assert.doesNotMatch(controller + moderation, /\$_(?:POST|REQUEST)\[[^\]]*captcha[^\]]*verified|set_transient\([^\n]*comment/i);
});
@@ -42,22 +42,22 @@ test('password and email account flows verify captcha once per distinct HTTP req
return account.slice(start, end === -1 ? account.length : end);
};
const send = methodBody('wp_ajax_aksrt_send_account_email_code', 'wp_ajax_aksrt_verify_account_email');
const password = methodBody('wp_ajax_aksrt_change_password', 'wp_ajax_aksrt_change_email');
const email = methodBody('wp_ajax_aksrt_change_email');
const send = methodBody('wp_ajax_jio_send_account_email_code', 'wp_ajax_jio_verify_account_email');
const password = methodBody('wp_ajax_jio_change_password', 'wp_ajax_jio_change_email');
const email = methodBody('wp_ajax_jio_change_email');
assert.equal((send.match(/aksrt_require_captcha\(/g) || []).length, 1);
assert.match(send, /aksrt_require_captcha\(\s*'change_email'/);
assert.equal((password.match(/aksrt_require_captcha\(/g) || []).length, 1);
assert.match(password, /aksrt_require_captcha\(\s*'change_password'/);
assert.equal((email.match(/aksrt_require_captcha\(/g) || []).length, 1);
assert.match(email, /aksrt_require_captcha\(\s*'change_email'/);
assert.equal((send.match(/jio_require_captcha\(/g) || []).length, 1);
assert.match(send, /jio_require_captcha\(\s*'change_email'/);
assert.equal((password.match(/jio_require_captcha\(/g) || []).length, 1);
assert.match(password, /jio_require_captcha\(\s*'change_password'/);
assert.equal((email.match(/jio_require_captcha\(/g) || []).length, 1);
assert.match(email, /jio_require_captcha\(\s*'change_email'/);
});
test('legacy public SMS-code AJAX surface is removed while SMS service remains', () => {
const sms = read('src/Sms/SMS.php');
assert.doesNotMatch(sms, /aksrt_send_sms_code/);
assert.doesNotMatch(sms, /jio_send_sms_code/);
assert.doesNotMatch(sms, /function ajax_send_code\s*\(/);
assert.match(sms, /public function send_code\(string \$phone, string \$code\): array/);
});
@@ -67,8 +67,8 @@ test('ticket create and reply endpoints verify captcha once per request', () =>
// 两个新场景已在场景白名单与后台「启用场景」勾选列表同步注册
const scenes = read('inc/modules/captcha.php');
assert.match(scenes, /'ticket_create'\s*=> __\('提交工单', 'aksrt'\)/);
assert.match(scenes, /'ticket_reply'\s*=> __\('回复工单', 'aksrt'\)/);
assert.match(scenes, /'ticket_create'\s*=> __\('提交工单', 'jio'\)/);
assert.match(scenes, /'ticket_reply'\s*=> __\('回复工单', 'jio'\)/);
const permissions = read('src/Admin/Schema/SchemaPermissions.php');
assert.match(permissions, /'label' => '提交工单', 'value' => 'ticket_create'/);
assert.match(permissions, /'label' => '回复工单', 'value' => 'ticket_reply'/);
@@ -76,8 +76,8 @@ test('ticket create and reply endpoints verify captcha once per request', () =>
assert.match(adminSchema, /label: '提交工单', value: 'ticket_create'/);
assert.match(adminSchema, /label: '回复工单', value: 'ticket_reply'/);
// REST 新建 / 回复各过一次场景校验(统一入口 helper 内恰一次 aksrt_require_captcha);
// 场景未启用时 aksrt_require_captcha 直接放行
// REST 新建 / 回复各过一次场景校验(统一入口 helper 内恰一次 jio_require_captcha);
// 场景未启用时 jio_require_captcha 直接放行
const submit = service.slice(service.indexOf('public function create_ticket'), service.indexOf('public function reply'));
const cancel = service.slice(service.indexOf('public function reply'), service.indexOf('public function urge'));
assert.match(submit, /require_captcha\(\$request, self::CAPTCHA_SCENE_CREATE\)/);
@@ -85,6 +85,6 @@ test('ticket create and reply endpoints verify captcha once per request', () =>
assert.match(service, /const CAPTCHA_SCENE_CREATE = 'ticket_create'/);
assert.match(service, /const CAPTCHA_SCENE_REPLY = 'ticket_reply'/);
assert.equal((service.match(/require_captcha\(\$request, self::CAPTCHA_SCENE_/g) || []).length, 2);
assert.equal((service.match(/aksrt_require_captcha\(/g) || []).length, 1);
assert.match(service, /function_exists\('aksrt_require_captcha'\)/);
assert.equal((service.match(/jio_require_captcha\(/g) || []).length, 1);
assert.match(service, /function_exists\('jio_require_captcha'\)/);
});
@@ -18,13 +18,13 @@ test('captcha exchange binds provider tokens to scene browser and one-time busin
const module = read('inc/modules/captcha.php');
const front = read('assets/js/captcha-front.js');
assert.match(module, /\/captcha\/exchange/);
assert.match(module, /aksrt_captcha_issue_challenge/);
assert.match(module, /aksrt_captcha_validate_challenge/);
assert.match(module, /aksrt_captcha_issue_proof/);
assert.match(module, /aksrt_captcha_consume_proof/);
assert.match(module, /jio_captcha_issue_challenge/);
assert.match(module, /jio_captcha_validate_challenge/);
assert.match(module, /jio_captcha_issue_proof/);
assert.match(module, /jio_captcha_consume_proof/);
assert.match(module, /browser_hash/);
assert.match(front, /exchange_url/);
assert.match(front, /aksrt_captcha_proof/);
assert.match(front, /jio_captcha_proof/);
assert.match(front, /runLocked/);
});
@@ -32,24 +32,24 @@ test('captcha defaults are opt-in and provider metadata is centralized', () => {
const module = read('inc/modules/captcha.php');
const phpSchema = read('src/Admin/Schema/SchemaPermissions.php');
const uiSchema = read('assets/admin/src/schema/sections/permissions/index.js');
assert.match(module, /function aksrt_captcha_provider_metadata/);
assert.match(module, /function jio_captcha_provider_metadata/);
assert.match(module, /'enabled_scenarios'\s*=>\s*\[\]/);
assert.match(phpSchema, /'default'\s*=>\s*\[\]/);
assert.match(uiSchema, /enabled_scenarios:\s*\[\]/);
assert.match(phpSchema, /aksrt_captcha_provider_metadata/);
assert.match(phpSchema, /jio_captcha_provider_metadata/);
});
test('captcha verification is fail-closed with a true-or-WP_Error contract', () => {
const module = read('inc/modules/captcha.php');
assert.match(module, /return aksrt_captcha_error\('aksrt_captcha_config'/);
assert.match(module, /return jio_captcha_error\('jio_captcha_config'/);
assert.match(module, /@return true\|\\WP_Error/);
assert.doesNotMatch(module, /is_wp_error\(\$result\) \? \$result->get_error_message\(\) : true/);
});
test('cloudflare siteverify performs one request and strictly binds action, cdata, and hostname', () => {
const module = read('inc/modules/captcha.php');
const start = module.indexOf('function aksrt_verify_cloudflare');
const end = module.indexOf('function aksrt_verify_geetest', start);
const start = module.indexOf('function jio_verify_cloudflare');
const end = module.indexOf('function jio_verify_geetest', start);
const cloudflare = module.slice(start, end);
assert.equal((cloudflare.match(/wp_remote_post\(/g) || []).length, 1);
assert.match(cloudflare, /array_key_exists\('action'/);
@@ -72,7 +72,7 @@ test('nested captcha secrets are masked, preserved, explicitly clearable, and ex
assert.match(provider, /merge_captcha_provider_config/);
assert.match(provider, /\$value === '' \|\| \$value === '\*\*\*'/);
assert.match(settings, /SCHEMA_MANAGED_STANDALONE_OPTION_KEYS/);
assert.match(settings, /'aksrt_captcha_config'/);
assert.match(settings, /'jio_captcha_config'/);
assert.match(page, /captchaSecretsToClear/);
assert.match(page, /clear-secret/);
});
@@ -82,10 +82,10 @@ test('admin captcha test obtains a frontend token then calls the protected serve
const api = read('assets/admin/src/api/modules/schema.js');
const page = read('assets/admin/src/pages/SchemaPage.vue');
assert.match(controller, /'\/captcha\/test'/);
assert.match(controller, /aksrt_verify_captcha_provider/);
assert.match(controller, /jio_verify_captcha_provider/);
assert.match(api, /testCaptcha\(provider, token\)/);
assert.match(page, /submitCaptchaTest/);
assert.match(page, /action:\s*'aksrt-admin-test'/);
assert.match(page, /action:\s*'jio-admin-test'/);
assert.match(page, /cdata:\s*'admin-captcha-test'/);
assert.doesNotMatch(page, /配置正确/);
});
@@ -111,25 +111,25 @@ test('cloud captcha verify requests use a 5s timeout and log raw failures', () =
for (const timeout of timeouts) {
assert.strictEqual(timeout, 5);
}
assert.match(module, /error_log\('\[AKSRT Captcha\]/);
assert.match(module, /error_log\('\[Jio Captcha\]/);
});
test('business requests consume one-time proofs without a shared download bypass', () => {
const module = read('inc/modules/captcha.php');
assert.match(module, /aksrt_captcha_consume_proof/);
assert.doesNotMatch(module, /aksrt_captcha_pass_ttl|aksrt_captcha_pass_key|aksrt_captcha_pass_/);
assert.match(module, /jio_captcha_consume_proof/);
assert.doesNotMatch(module, /jio_captcha_pass_ttl|jio_captcha_pass_key|jio_captcha_pass_/);
});
test('cloudflare siteverify stays bound to the aksrt action and scene', () => {
test('cloudflare siteverify stays bound to the jio action and scene', () => {
const module = read('inc/modules/captcha.php');
assert.match(module, /aksrt_verify_cloudflare\(array \$pcfg, array \$post_data, string \$client_ip, string \$scene = ''(?:, string \$action = 'aksrt-verify')?\)/);
assert.match(module, /jio_verify_cloudflare\(array \$pcfg, array \$post_data, string \$client_ip, string \$scene = ''(?:, string \$action = 'jio-verify')?\)/);
assert.match(module, /action mismatch/);
assert.match(module, /cdata mismatch/);
});
test('captcha render is a data-driven mount without inline scripts or stylesheets', () => {
const module = read('inc/modules/captcha.php');
assert.match(module, /data-aksrt-captcha-config/);
assert.match(module, /data-jio-captcha-config/);
assert.doesNotMatch(module, /<script|<link rel/);
});
@@ -143,17 +143,17 @@ test('captcha front runtime ships inside the public bundle', () => {
assert.match(build, /'assets\/js\/captcha-front\.js'/);
assert.match(build, /'assets\/css\/shared\/captcha\.css'/);
const front = read('assets/js/captcha-front.js');
assert.match(front, /AKSRTCaptcha/);
assert.match(front, /data-aksrt-captcha-config/);
assert.match(front, /JioCaptcha/);
assert.match(front, /data-jio-captcha-config/);
assert.match(front, /cdata: C\.scene/, 'turnstile widget must bind cdata to the scene');
});
test('captcha public runtime exposes only the shared API and resolves scoped uid instances', () => {
const front = read('assets/js/captcha-front.js');
assert.match(front, /window\.AKSRTCaptcha\s*=\s*Object\.freeze\(\{[\s\S]*verify:\s*verify,[\s\S]*collect:\s*collect,[\s\S]*reset:\s*reset,[\s\S]*run:\s*run,[\s\S]*TOKEN_FIELDS:\s*TOKEN_FIELDS/);
assert.match(front, /window\.JioCaptcha\s*=\s*Object\.freeze\(\{[\s\S]*verify:\s*verify,[\s\S]*collect:\s*collect,[\s\S]*reset:\s*reset,[\s\S]*run:\s*run,[\s\S]*TOKEN_FIELDS:\s*TOKEN_FIELDS/);
assert.match(front, /instances\[instance\.uid\]\s*=\s*instance/);
assert.match(front, /function resolve\(scene, scope\)/);
assert.doesNotMatch(front, /AKSRTCaptchaState/);
assert.doesNotMatch(front, /JioCaptchaState/);
assert.doesNotMatch(front, /tencent_sid/);
});
@@ -166,9 +166,9 @@ test('captcha consumers use the shared API and reset every token-bearing request
'assets/js/signin.js',
'assets/js/front-submit.js',
].map(read).join('\n');
assert.match(consumers, /AKSRTCaptcha\.run|captcha\.run/);
assert.match(consumers, /AKSRTCaptcha\.reset|captcha\.reset|resetCaptcha/);
assert.doesNotMatch(consumers, /AKSRTCaptchaState/);
assert.match(consumers, /JioCaptcha\.run|captcha\.run/);
assert.match(consumers, /JioCaptcha\.reset|captcha\.reset|resetCaptcha/);
assert.doesNotMatch(consumers, /JioCaptchaState/);
assert.doesNotMatch(consumers, /tencent_sid/);
assert.doesNotMatch(read('assets/js/core/comments.js'), /wp-comments-post|performLegacySubmit/);
});
@@ -177,10 +177,10 @@ test('sign-in waits for main and fails closed when its enabled captcha mount is
const signin = read('assets/js/signin.js');
const scripts = read('src/Core/ScriptsPart1c.php');
assert.match(signin, /tbquire\(\['main'\], boot\)/);
assert.match(signin, /var captchaRequired = !!window\.aksrtData\.captchaSignin/);
assert.match(signin, /var captchaRequired = !!window\.jioData\.captchaSignin/);
assert.match(signin, /captchaRequired && \(!captcha \|\| !mount\)[\s\S]*?return;/);
assert.doesNotMatch(signin, /captcha \? captcha\.run\('signin', mount\) : Promise\.resolve\(\{\}\)/);
assert.match(scripts, /'captchaSignin'\s*=>\s*function_exists\('aksrt_is_captcha_enabled_for'\)\s*&&\s*aksrt_is_captcha_enabled_for\('signin'\)/);
assert.match(scripts, /'captchaSignin'\s*=>\s*function_exists\('jio_is_captcha_enabled_for'\)\s*&&\s*jio_is_captcha_enabled_for\('signin'\)/);
});
test('auth and account security flows consume fresh captcha tickets', () => {
@@ -16,8 +16,8 @@ function createInput(name, value = '') {
function createHarness() {
const fields = new Map();
const challenge = createInput('aksrt_captcha_challenge', 'challenge-token');
const proof = createInput('aksrt_captcha_proof', '');
const challenge = createInput('jio_captcha_challenge', 'challenge-token');
const proof = createInput('jio_captcha_proof', '');
const verified = createInput('captcha_verified', '0');
fields.set(challenge.name, challenge);
fields.set(proof.name, proof);
@@ -56,7 +56,7 @@ function createHarness() {
const document = {
readyState: 'complete',
documentElement: { nodeType: 1 },
querySelectorAll(selector) { return selector === '[data-aksrt-captcha-config]' ? [mount] : []; },
querySelectorAll(selector) { return selector === '[data-jio-captcha-config]' ? [mount] : []; },
querySelector: () => null,
getElementById(id) { return id === 'captcha-box' ? box : verified; },
createElement(tag) {
@@ -88,7 +88,7 @@ function createHarness() {
Promise,
};
vm.runInNewContext(runtime, { window, document, Promise, MutationObserver: undefined, setTimeout, clearTimeout, console });
return { api: window.AKSRTCaptcha, container };
return { api: window.JioCaptcha, container };
}
test('captcha run is single-flight until the owning business request resets', async () => {
@@ -98,7 +98,7 @@ test('captcha run is single-flight until the owning business request resets', as
const first = api.run('login', container);
const second = api.run('login', container);
const firstPayload = await first;
assert.equal(firstPayload.aksrt_captcha_proof, 'proof-token');
assert.equal(firstPayload.jio_captcha_proof, 'proof-token');
let secondSettled = false;
second.then(() => { secondSettled = true; });
@@ -133,7 +133,7 @@ test('sign-in waits for main before binding and invokes the signin captcha', asy
};
const jqueryApi = { off() { return this; }, on() { return this; } };
const jQuery = () => jqueryApi;
const window = { aksrtData: { captchaSignin: true } };
const window = { jioData: { captchaSignin: true } };
let mainReady;
let captchaCall = null;
const tbquire = (deps, callback) => {
@@ -147,15 +147,15 @@ test('sign-in waits for main before binding and invokes the signin captcha', asy
assert.equal(listeners.click, undefined);
assert.equal(typeof mainReady, 'function');
window.AksrtRequest = {
window.JioRequest = {
request(endpoint) {
if (endpoint === '/signin/today') return Promise.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 0, reward_if_signin: 1 } });
if (endpoint === '/signin') return Promise.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1 } });
throw new Error('unexpected endpoint ' + endpoint);
},
};
window.AKSRTCaptcha = {
run(scene, scope) { captchaCall = { scene, scope }; return Promise.resolve({ aksrt_captcha_proof: 'proof' }); },
window.JioCaptcha = {
run(scene, scope) { captchaCall = { scene, scope }; return Promise.resolve({ jio_captcha_proof: 'proof' }); },
reset() {},
};
@@ -10,7 +10,7 @@ namespace {
define('MINUTE_IN_SECONDS', 60);
define('MONTH_IN_SECONDS', 2592000);
define('ARRAY_A', 'ARRAY_A');
$GLOBALS['aksrt_test_options'] = [];
$GLOBALS['jio_test_options'] = [];
$GLOBALS['wp_cache'] = [];
final class HarnessWpdb {
@@ -21,8 +21,8 @@ namespace {
public function query($prepared): int {
[, $args] = $prepared;
[$key, $serialized] = $args;
if (!array_key_exists($key, $GLOBALS['aksrt_test_options']) || serialize($GLOBALS['aksrt_test_options'][$key]) !== $serialized) return 0;
unset($GLOBALS['aksrt_test_options'][$key]);
if (!array_key_exists($key, $GLOBALS['jio_test_options']) || serialize($GLOBALS['jio_test_options'][$key]) !== $serialized) return 0;
unset($GLOBALS['jio_test_options'][$key]);
return 1;
}
}
@@ -42,16 +42,16 @@ namespace {
function wp_kses_post($value): string { return strip_tags((string) $value, '<b><strong><em><i><a><p><br>'); }
function esc_url_raw($value): string { return trim((string) $value); }
function wp_unslash($value) { return $value; }
function get_option(string $key, $default = false) { return array_key_exists($key, $GLOBALS['aksrt_test_options']) ? $GLOBALS['aksrt_test_options'][$key] : $default; }
function update_option(string $key, $value): bool { $GLOBALS['aksrt_test_options'][$key] = $value; return true; }
function get_option(string $key, $default = false) { return array_key_exists($key, $GLOBALS['jio_test_options']) ? $GLOBALS['jio_test_options'][$key] : $default; }
function update_option(string $key, $value): bool { $GLOBALS['jio_test_options'][$key] = $value; return true; }
function add_option(string $key, $value, string $deprecated = '', bool $autoload = true): bool {
if (array_key_exists($key, $GLOBALS['aksrt_test_options'])) return false;
$GLOBALS['aksrt_test_options'][$key] = $value;
if (array_key_exists($key, $GLOBALS['jio_test_options'])) return false;
$GLOBALS['jio_test_options'][$key] = $value;
return true;
}
function delete_option(string $key): bool {
if (!array_key_exists($key, $GLOBALS['aksrt_test_options'])) return false;
unset($GLOBALS['aksrt_test_options'][$key]);
if (!array_key_exists($key, $GLOBALS['jio_test_options'])) return false;
unset($GLOBALS['jio_test_options'][$key]);
return true;
}
function maybe_serialize($value): string { return serialize($value); }
@@ -77,7 +77,7 @@ namespace {
function wp_unique_id(string $prefix = ''): string { return $prefix . '1'; }
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function flush_settings_cache(): void {}
public static function sync_native_site_icon(string $value): void {}
@@ -99,10 +99,10 @@ namespace {
}
}
check(aksrt_captcha_default_config()['enabled_scenarios'] === [], 'captcha scenarios are not opt-in');
check(!in_array('domain', array_keys(aksrt_captcha_provider_defaults('cloudflare')), true), 'Cloudflare domain remains allowed');
check(jio_captcha_default_config()['enabled_scenarios'] === [], 'captcha scenarios are not opt-in');
check(!in_array('domain', array_keys(jio_captcha_provider_defaults('cloudflare')), true), 'Cloudflare domain remains allowed');
$GLOBALS['aksrt_test_options']['aksrt_captcha_config'] = [
$GLOBALS['jio_test_options']['jio_captcha_config'] = [
'enabled' => true,
'active_provider' => 'cloudflare',
'enabled_scenarios' => ['login'],
@@ -114,9 +114,9 @@ namespace {
'domain' => 'should-be-dropped.test',
],
];
aksrt_captcha_config_reset();
jio_captcha_config_reset();
$provider = \AKSRT\Admin\SchemaProvider::get_instance();
$provider = \Jio\Admin\SchemaProvider::get_instance();
$read = $provider->read_settings('permissions', 'login');
check(($read['cloudflare']['secret_key'] ?? '') === '***', 'nested secret was not masked');
check(!isset($read['cloudflare']['domain']), 'unknown provider key was exposed');
@@ -130,7 +130,7 @@ namespace {
'domain' => 'ignored.test',
],
], 'login');
$stored = $GLOBALS['aksrt_test_options']['aksrt_captcha_config']['cloudflare'];
$stored = $GLOBALS['jio_test_options']['jio_captcha_config']['cloudflare'];
check($stored['secret_key'] === 'stored-secret', 'masked secret did not preserve stored value');
check($stored['site_key'] === 'new-site-key', 'public provider field was not updated');
check(!isset($stored['domain']), 'unknown provider key was persisted');
@@ -138,47 +138,47 @@ namespace {
$provider->write_settings('permissions', [
'cloudflare' => ['secret_key' => ''],
], 'login');
check($GLOBALS['aksrt_test_options']['aksrt_captcha_config']['cloudflare']['secret_key'] === 'stored-secret', 'empty secret did not preserve stored value');
check($GLOBALS['jio_test_options']['jio_captcha_config']['cloudflare']['secret_key'] === 'stored-secret', 'empty secret did not preserve stored value');
$provider->write_settings('permissions', [
'__clear_secrets' => ['cloudflare.secret_key'],
], 'login');
check($GLOBALS['aksrt_test_options']['aksrt_captcha_config']['cloudflare']['secret_key'] === '', 'explicit nested secret clear failed');
check($GLOBALS['jio_test_options']['jio_captcha_config']['cloudflare']['secret_key'] === '', 'explicit nested secret clear failed');
$GLOBALS['aksrt_test_options']['aksrt_captcha_config'] = [
$GLOBALS['jio_test_options']['jio_captcha_config'] = [
'enabled' => true,
'active_provider' => '',
'enabled_scenarios' => ['login'],
];
aksrt_captcha_config_reset();
$failed_closed = aksrt_require_captcha('login', []);
jio_captcha_config_reset();
$failed_closed = jio_require_captcha('login', []);
check(is_wp_error($failed_closed), 'enabled scene with missing provider failed open');
check($failed_closed->get_error_code() === 'aksrt_captcha_config', 'fail-closed error code mismatch');
check($failed_closed->get_error_code() === 'jio_captcha_config', 'fail-closed error code mismatch');
$GLOBALS['aksrt_test_options']['aksrt_captcha_config'] = [
$GLOBALS['jio_test_options']['jio_captcha_config'] = [
'enabled' => true,
'active_provider' => 'cloudflare',
'enabled_scenarios' => ['login', 'comment'],
'cloudflare' => ['site_key' => 'site', 'secret_key' => 'secret', 'theme' => 'auto', 'size' => 'normal'],
];
aksrt_captcha_config_reset();
$_COOKIE[AKSRT_CAPTCHA_BROWSER_COOKIE] = str_repeat('A', 43);
$challenge = aksrt_captcha_issue_challenge('login', 'cloudflare');
check(aksrt_captcha_validate_challenge($challenge, 'login', 'cloudflare', false), 'valid scene challenge failed');
check(!aksrt_captcha_validate_challenge($challenge, 'comment', 'cloudflare', false), 'cross-scene challenge accepted');
check(!aksrt_captcha_validate_challenge($challenge, 'login', 'geetest', false), 'cross-provider challenge accepted');
$_COOKIE[AKSRT_CAPTCHA_BROWSER_COOKIE] = str_repeat('B', 43);
check(!aksrt_captcha_validate_challenge($challenge, 'login', 'cloudflare', false), 'cross-browser challenge accepted');
$_COOKIE[AKSRT_CAPTCHA_BROWSER_COOKIE] = str_repeat('A', 43);
check(aksrt_captcha_validate_challenge($challenge, 'login', 'cloudflare', true), 'valid challenge consume failed');
check(!aksrt_captcha_validate_challenge($challenge, 'login', 'cloudflare', true), 'challenge replay accepted');
jio_captcha_config_reset();
$_COOKIE[JIO_CAPTCHA_BROWSER_COOKIE] = str_repeat('A', 43);
$challenge = jio_captcha_issue_challenge('login', 'cloudflare');
check(jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', false), 'valid scene challenge failed');
check(!jio_captcha_validate_challenge($challenge, 'comment', 'cloudflare', false), 'cross-scene challenge accepted');
check(!jio_captcha_validate_challenge($challenge, 'login', 'geetest', false), 'cross-provider challenge accepted');
$_COOKIE[JIO_CAPTCHA_BROWSER_COOKIE] = str_repeat('B', 43);
check(!jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', false), 'cross-browser challenge accepted');
$_COOKIE[JIO_CAPTCHA_BROWSER_COOKIE] = str_repeat('A', 43);
check(jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', true), 'valid challenge consume failed');
check(!jio_captcha_validate_challenge($challenge, 'login', 'cloudflare', true), 'challenge replay accepted');
$proof = aksrt_captcha_issue_proof('login', 'cloudflare');
check(!aksrt_captcha_consume_proof($proof, 'comment', 'cloudflare'), 'cross-scene proof accepted');
check(aksrt_captcha_consume_proof($proof, 'login', 'cloudflare'), 'valid proof consume failed');
check(!aksrt_captcha_consume_proof($proof, 'login', 'cloudflare'), 'proof replay accepted');
$proof = jio_captcha_issue_proof('login', 'cloudflare');
check(!jio_captcha_consume_proof($proof, 'comment', 'cloudflare'), 'cross-scene proof accepted');
check(jio_captcha_consume_proof($proof, 'login', 'cloudflare'), 'valid proof consume failed');
check(!jio_captcha_consume_proof($proof, 'login', 'cloudflare'), 'proof replay accepted');
$headers = aksrt_tencent_tc3_sign('id', 'key', 'captcha', 1700000000, '2023-11-14', 'DescribeCaptchaResult', '2019-07-22', '{}');
$headers = jio_tencent_tc3_sign('id', 'key', 'captcha', 1700000000, '2023-11-14', 'DescribeCaptchaResult', '2019-07-22', '{}');
check(str_starts_with($headers['Authorization'], 'TC3-HMAC-SHA256 Credential=id/2023-11-14/captcha/tc3_request'), 'TC3 authorization scope mismatch');
echo "Captcha security harness passed.\n";
@@ -30,7 +30,7 @@ const payVipJs = read('assets', 'js', 'pay-vip.js');
const chargeCard = read('src', 'Payment', 'ChargeCard.php');
const userOrders = read('src', 'Payment', 'UserOrders.php');
const vipPackages = read('src', 'Content', 'VipServicePackages.php');
const aksrtPay = read('inc', 'helpers', 'aksrt-pay.php');
const jioPay = read('inc', 'helpers', 'jio-pay.php');
const mainCss = read('assets', 'css', 'main.css');
const vipPurchase = read('src', 'Content', 'VipServicePurchase.php');
const vipModal = read('src', 'Content', 'VipServiceModal.php');
@@ -44,9 +44,9 @@ test('独立入口与通用弹窗已删除', () => { assert.equal(
'credits-vip.php 死文件应已删除'
);
// 通用卡密弹窗与 initCardPass 不回潮。
assert.doesNotMatch(wallet, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
assert.doesNotMatch(wallet, /data-jio-card-pass-modal|data-jio-card-redeem/);
assert.doesNotMatch(creditsJs, /function initCardPass\(|initCardPass\(\)/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
assert.doesNotMatch(creditsRenderer, /data-jio-card-pass-modal|data-jio-card-redeem/);
// 卡密 logo 就位(三个弹窗共用)。
assert.equal(fs.existsSync(path.join(root, 'assets', 'img', 'pay-card-pass-logo.svg')), true);
});
@@ -67,13 +67,13 @@ test('余额充值弹窗:卡密渠道为支付方式之一,卡号/卡密输
test('购买积分弹窗:Zibll points-pay modal 结构 + 卡密渠道', () => {
// 商品/自定义选择 + 支付方式 + hidden + 立即支付。
assert.match(creditsRenderer, /data-aksrt-points-pay-modal/);
assert.match(creditsRenderer, /data-aksrt-points-pay-form/);
assert.match(creditsRenderer, /data-aksrt-points-pay-open/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-points-exchange-(open|modal|form|amount|preview|close)/);
assert.match(creditsRenderer, /data-jio-points-pay-modal/);
assert.match(creditsRenderer, /data-jio-points-pay-form/);
assert.match(creditsRenderer, /data-jio-points-pay-open/);
assert.doesNotMatch(creditsRenderer, /data-jio-points-exchange-(open|modal|form|amount|preview|close)/);
// 商品/自定义提示语跟随自定义货币名称(后台「积分配置 → 自定义货币名称」)。
assert.match(creditsRenderer, /请选择需购买的%s', 'aksrt'\), esc_html\(\$currency\)/);
assert.match(creditsRenderer, /请输入需购买的%s数额', 'aksrt'\), esc_html\(\$currency\)/);
assert.match(creditsRenderer, /请选择需购买的%s', 'jio'\), esc_html\(\$currency\)/);
assert.match(creditsRenderer, /请输入需购买的%s数额', 'jio'\), esc_html\(\$currency\)/);
assert.match(creditsRenderer, /name="points_product"/);
assert.match(creditsRenderer, /name="points_custom"/);
assert.match(creditsRenderer, /name="payment_method"/);
@@ -86,7 +86,7 @@ test('购买积分弹窗:Zibll points-pay modal 结构 + 卡密渠道', () =>
test('购买积分前端:商品/支付方式选中模型 + 余额/卡密提交分流', () => {
assert.match(creditsJs, /function initPointsPay\(\)/);
assert.match(creditsJs, /data-aksrt-points-pay-open/);
assert.match(creditsJs, /data-jio-points-pay-open/);
assert.match(creditsJs, /\[data-for="points_product"\]/);
// 卡密选中:隐藏 .charge-box、显示卡密输入盒(契约与参考主题一致)。
assert.match(creditsJs, /data-controller="payment_method"\]\[data-value="card_pass"/);
@@ -102,7 +102,7 @@ test('购买积分前端:商品/支付方式选中模型 + 余额/卡密提交
test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单)', () => {
// 后端:available_payment_methods 追加 card_pass + 视图模型透传。
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'aksrt'\)/);
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'jio'\)/);
assert.match(vipPackages, /function vip_card_pass_context\(\)/);
assert.match(vipPackages, /vip_card_pass_context\(\)/);
assert.match(vipModal, /'card_pass' => \$card_pass/);
@@ -113,7 +113,7 @@ test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单
assert.match(vipModal, /'wallet_balance' => \(string\) \(\$catalog\['wallet_balance'\]/);
assert.match(vipModal, /'currency_mark' =>/);
// 后端防御:purchase() 拒绝 card_pass(卡密直兑走 redeem)。
assert.match(vipPurchase, /aksrt_card_pass_redeem_required/);
assert.match(vipPurchase, /jio_card_pass_redeem_required/);
// 前端:Zibll initiate_pay_input 同构组件——payment-method-radio logo 行、
// 我的余额盒(data-controller == balance)、卡密盒(password_box 类结构)、
// hidden payment_method、initiate-pay 按钮。
@@ -126,15 +126,15 @@ test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单
assert.match(payVipJs, /"data-value", "card_pass"/);
assert.match(payVipJs, /hidden\.name = "payment_method"/);
assert.match(payVipJs, /mt6 but jb-red initiate-pay btn-block radius/);
assert.doesNotMatch(payVipJs, /uc-vip-shop__method|aksrt_vip_pay_method|aksrt-vip-submit/);
assert.doesNotMatch(payVipJs, /uc-vip-shop__method|jio_vip_pay_method|jio-vip-submit/);
// 按钮字样对齐参考主题:会员弹窗即 initiate_pay_input(4) 默认「立即支付」;
// 无可用支付方式渲染 Zibll 同款 badg 提示(而非禁用按钮/自实现文案)。
assert.doesNotMatch(payVipJs, /cta_label/);
assert.match(payVipJs, /canPurchase \? "立即支付"/);
assert.match(payVipJs, /badg px12 c-yellow-2/, "暂时无法支付,请与客服联系");
// 渠道展示名对齐参考主题 zibpay_get_payment_method_args(余额/微信/支付宝)。
assert.match(aksrtPay, /'balance'\s+=> __\('余额', 'aksrt'\)/);
assert.match(aksrtPay, /'wechatpay'\s+=> __\('微信', 'aksrt'\)/);
assert.match(jioPay, /'balance'\s+=> __\('余额', 'jio'\)/);
assert.match(jioPay, /'wechatpay'\s+=> __\('微信', 'jio'\)/);
// 显隐:通用 data-controller(==/!=)匹配 + 卡密选中隐藏套餐。
assert.match(payVipJs, /function applyVipMethodVisibility\(/);
assert.match(payVipJs, /querySelectorAll\('\[data-controller="payment_method"\]'\)/);
@@ -205,7 +205,7 @@ test('核销订单契约:金额恒为 0,订单名称按 note 生成', () =>
});
test('多渠道防挤压:图标 flex:none + 行不换行项目收缩成芯片(对齐 Zibll)', () => {
// 图标禁用 flex 压缩并抵消 AKSRT bootstrap 全局 img{max-width:100%}
// 图标禁用 flex 压缩并抵消 Jio bootstrap 全局 img{max-width:100%}
// (flex 压缩时把图标钳制到被压窄的项宽,实测 5 渠道 400px 弹窗图标
// 被压成 16px 细条)。行保持 Zibll 原样不换行:空间不足时项目收缩为
// 「图标上/文字下」芯片(实测 5 渠道一行 5 个、6 渠道一行 6 个,图标
@@ -20,14 +20,14 @@ function walk(dir, exts, out = []) {
return out;
}
test('n-color-picker is used only inside the shared AksColorPicker', () => {
test('n-color-picker is used only inside the shared JioColorPicker', () => {
const offenders = walk('assets/admin/src', ['.vue', '.js'])
.filter((file) => /n-color-picker|NColorPicker/.test(read(file)))
.map((file) => file.replace(/\\/g, '/'));
assert.deepEqual(offenders, ['assets/admin/src/components/form/AksColorPicker.vue']);
assert.deepEqual(offenders, ['assets/admin/src/components/form/JioColorPicker.vue']);
});
test('PHP color sanitizing goes through AKSRT\\Core\\Color only', () => {
test('PHP color sanitizing goes through Jio\\Core\\Color only', () => {
const offenders = [...walk('src', ['.php']), ...walk('inc', ['.php'])]
.filter((file) => /sanitize_hex_color\(|rgba\?\\\(/.test(read(file)))
.map((file) => file.replace(/\\/g, '/'))
@@ -38,15 +38,15 @@ test('PHP color sanitizing goes through AKSRT\\Core\\Color only', () => {
test('schema color fields sanitize via Color with showAlpha opt-in', () => {
const provider = read('src/Admin/SchemaProvider.php');
assert.match(provider, /'color'\s*=>\s*'color'/);
assert.match(provider, /case 'color_alpha':\s*return \\AKSRT\\Core\\Color::sanitize\(\$value, true\)/);
assert.match(provider, /case 'color_alpha':\s*return \\Jio\\Core\\Color::sanitize\(\$value, true\)/);
assert.match(provider, /showAlpha/);
const field = read('assets/admin/src/components/form/AksField.vue');
assert.match(field, /<AksColorPicker[\s\S]*?:alpha="!!schema\.showAlpha"/);
const field = read('assets/admin/src/components/form/JioField.vue');
assert.match(field, /<JioColorPicker[\s\S]*?:alpha="!!schema\.showAlpha"/);
});
test('dead composite branches are gone', () => {
const composite = read('assets/admin/src/components/form/AksCompositeField.vue');
assert.doesNotMatch(composite, /'dimensions'|'spacing'|'background'|AksMediaField/);
const composite = read('assets/admin/src/components/form/JioCompositeField.vue');
assert.doesNotMatch(composite, /'dimensions'|'spacing'|'background'|JioMediaField/);
assert.doesNotMatch(read('assets/js/gutenberg-blocks.js'), /ColorPicker/);
});
@@ -61,7 +61,7 @@ test('JS normalizeColor mirrors PHP Color::sanitize', async () => {
['red', true], ['', true], [' #FFF ', false],
];
const php = `require 'src/Core/Color.php';`
+ `foreach (json_decode($argv[1], true) as [$v, $a]) { echo AKSRT\\Core\\Color::sanitize($v, $a), "\\n"; }`;
+ `foreach (json_decode($argv[1], true) as [$v, $a]) { echo Jio\\Core\\Color::sanitize($v, $a), "\\n"; }`;
const out = execFileSync('php', ['-d', 'display_errors=1', '-r', `define('ABSPATH', '/');${php}`, JSON.stringify(cases)], {
cwd: root,
encoding: 'utf8',
@@ -71,17 +71,17 @@ test('JS normalizeColor mirrors PHP Color::sanitize', async () => {
test('widget background color input is wpColorPicker-enhanced like top-level color fields', () => {
const renderer = read('src/WidgetSystem/FormRenderer.php');
assert.match(renderer, /\$key \. '\[color\]'[^\n]+'widefat aksrt-widget-color'\)/);
assert.match(renderer, /\$key \. '\[color\]'[^\n]+'widefat jio-widget-color'\)/);
assert.match(renderer, /class="' \. esc_attr\(\$class\) \. '"/);
assert.match(read('assets/widgets/admin.js'), /\.aksrt-widget-color'\)\.wpColorPicker\(\)/);
assert.match(read('assets/widgets/admin.js'), /\.jio-widget-color'\)\.wpColorPicker\(\)/);
});
test('page config nav color uses wpColorPicker instead of native input[type=color]', () => {
const metaBox = read('src/Admin/PageConfigMetaBox.php');
assert.doesNotMatch(metaBox, /type="color"/);
assert.match(metaBox, /class="aksrt-page-config-color" type="text" name="page_config\[header_slider_nav_color\]"/);
assert.match(metaBox, /class="jio-page-config-color" type="text" name="page_config\[header_slider_nav_color\]"/);
assert.match(metaBox, /wp_enqueue_script\('wp-color-picker'\)/);
assert.match(metaBox, /\.aksrt-page-config-color'\)\.wpColorPicker\(\)/);
assert.match(metaBox, /\.jio-page-config-color'\)\.wpColorPicker\(\)/);
assert.match(metaBox, /Color::sanitize\(\(string\) \(\$raw\['header_slider_nav_color'\]/);
});
@@ -98,7 +98,7 @@ test('quick reply is removed across the owned comment stack', () => {
'assets/css/main.css',
'assets/css/build/comments.css',
'assets/js/comment-tools.js',
'inc/helpers/aksrt-comments.php',
'inc/helpers/jio-comments.php',
'inc/helpers/comments/form-parts.php',
'src/Admin/Schema/SchemaPostList.php',
'src/Core/ScriptsPart1c.php',
@@ -157,28 +157,28 @@ test('comment markup keeps the 1:1 legacy DOM contract', () => {
const item = read('inc/helpers/comments/item.php');
// 点赞按钮(legacy: zib_get_comment_like — svg + text + count)。
assert.ok(item.includes("<text></text><count>"), 'like button must render <text> before <count>');
assert.ok(item.includes('aksrt_get_comment_icon(\'like\''), 'like button must use the sprite icon');
assert.ok(item.includes('aksrt_get_comment_icon(\'topping\')'), 'topping badge must use the sprite icon');
assert.ok(item.includes('aksrt_get_comment_icon(\'approve\''), 'approve action must use the sprite icon');
assert.ok(item.includes('aksrt_get_comment_icon(\'menu_2\')'), 'action dropdown must use the sprite icon');
assert.ok(item.includes('jio_get_comment_icon(\'like\''), 'like button must use the sprite icon');
assert.ok(item.includes('jio_get_comment_icon(\'topping\')'), 'topping badge must use the sprite icon');
assert.ok(item.includes('jio_get_comment_icon(\'approve\''), 'approve action must use the sprite icon');
assert.ok(item.includes('jio_get_comment_icon(\'menu_2\')'), 'action dropdown must use the sprite icon');
assert.ok(item.includes('data-toggle="tooltip"'), 'time/reply tooltip parity');
assert.equal(item.includes('fa fa-thumbs-up'), false, 'legacy fa like icon must be gone');
const comments = read('inc/helpers/aksrt-comments.php');
const comments = read('inc/helpers/jio-comments.php');
assert.ok(comments.includes('post_password_required() || !comments_open()'), 'closed comments must render nothing');
assert.ok(comments.includes('user_url_enabled'), 'guest url field toggle must be wired');
assert.ok(comments.includes("name=\"url\""), 'guest url input must render');
assert.ok(comments.includes("aksrt_get_comment_icon('signup', 'icon mr10')"), 'signarea signup must use the sprite icon');
assert.ok(comments.includes("aksrt_get_vip_icon($current_user->ID, 'em12 mr3')"), 'comt-title must render the direct VIP image with reference-theme spacing');
assert.ok(comments.includes("aksrt_get_avatar_html($current_user->ID, 96, ['class' => 'avatar'])"), 'logged-in avatar must use the canonical avatar helper at the reference source size');
assert.ok(comments.includes("aksrt_get_avatar_html(0, 96, ['class' => 'avatar'])"), 'guest avatar must use the canonical default-avatar helper');
assert.equal(comments.includes('aksrt_render_vip_badge'), false, 'legacy VIP wrapper helper must not remain in the comment form');
assert.equal(comments.includes('aksrt_render_user_identity_chips'), false, 'identity chips must not crowd the narrow comt-title');
assert.ok(comments.includes("jio_get_comment_icon('signup', 'icon mr10')"), 'signarea signup must use the sprite icon');
assert.ok(comments.includes("jio_get_vip_icon($current_user->ID, 'em12 mr3')"), 'comt-title must render the direct VIP image with reference-theme spacing');
assert.ok(comments.includes("jio_get_avatar_html($current_user->ID, 96, ['class' => 'avatar'])"), 'logged-in avatar must use the canonical avatar helper at the reference source size');
assert.ok(comments.includes("jio_get_avatar_html(0, 96, ['class' => 'avatar'])"), 'guest avatar must use the canonical default-avatar helper');
assert.equal(comments.includes('jio_render_vip_badge'), false, 'legacy VIP wrapper helper must not remain in the comment form');
assert.equal(comments.includes('jio_render_user_identity_chips'), false, 'identity chips must not crowd the narrow comt-title');
const vip = read('inc/helpers/aksrt-vip.php');
const vip = read('inc/helpers/jio-vip.php');
assert.ok(vip.includes("trim('img-icon ' . $class)"), 'VIP helper must emit the reference img-icon contract');
assert.equal(vip.includes('function aksrt_render_vip_badge'), false, 'legacy wrapped VIP helper must be removed');
assert.equal(vip.includes('<span class="aksrt-vip-badge'), false, 'VIP helper must not add a badge wrapper');
assert.equal(vip.includes('function jio_render_vip_badge'), false, 'legacy wrapped VIP helper must be removed');
assert.equal(vip.includes('<span class="jio-vip-badge'), false, 'VIP helper must not add a badge wrapper');
const formParts = read('inc/helpers/comments/form-parts.php');
assert.ok(formParts.includes("case 'link':"), 'link dropdown case must exist');
@@ -186,7 +186,7 @@ test('comment markup keeps the 1:1 legacy DOM contract', () => {
assert.equal(formParts.includes('fa-bolt'), false, 'quick button icon must be gone');
const icons = read('inc/helpers/comments/icons.php');
assert.ok(icons.includes('function aksrt_get_comment_icon'), 'sprite icon helper must exist');
assert.ok(icons.includes('function jio_get_comment_icon'), 'sprite icon helper must exist');
const bootstrap = read('inc/bootstrap.php');
assert.ok(bootstrap.includes("comments/icons.php"), 'bootstrap must load the icon helper');
@@ -25,7 +25,7 @@ test('comment image settings use one two-switch contract', () => {
}
assert.match(display, /!empty\(\$settings\['comment_upload_img'\]\)/);
assert.match(display, /is_user_logged_in\(\)/);
assert.match(display, /aksrt_user_can\('upload_comment_image'/);
assert.match(display, /jio_user_can\('upload_comment_image'/);
assert.doesNotMatch(runtime, /commentUploadEnabled/);
assert.doesNotMatch(runtime, /'comment_upload_img'\s*=>/);
});
@@ -64,7 +64,7 @@ test('comment upload REST endpoint has explicit nonce, switch, capability, and i
assert.match(controller, /wp_verify_nonce\(\$nonce, 'wp_rest'\)/);
assert.match(controller, /\$settings\['comment_img'\]/);
assert.match(controller, /\$settings\['comment_upload_img'\]/);
assert.match(controller, /aksrt_user_can\('upload_comment_image'/);
assert.match(controller, /jio_user_can\('upload_comment_image'/);
assert.match(controller, /empty\(\$_FILES\['image'\]\)/);
assert.match(controller, /\$file_key = 'image'/);
assert.match(controller, /wp_delete_attachment\(\(int\) \$attachment_id, true\)/);
+17 -17
View File
@@ -16,16 +16,16 @@ final class WP_Comment_Query {
}
}
final class AKSRT_Test_WPDB {
final class JIO_Test_WPDB {
public string $comments = 'wp_comments';
public string $commentmeta = 'wp_commentmeta';
}
$GLOBALS['wpdb'] = new AKSRT_Test_WPDB();
$GLOBALS['aksrt_test_filters'] = [];
$GLOBALS['wpdb'] = new JIO_Test_WPDB();
$GLOBALS['jio_test_filters'] = [];
function add_filter(string $tag, callable $callback, int $priority = 10, int $accepted_args = 1): void {
$GLOBALS['aksrt_test_filters'][$tag][] = [$callback, $priority, $accepted_args];
$GLOBALS['jio_test_filters'][$tag][] = [$callback, $priority, $accepted_args];
}
function sanitize_key(string $key): string {
@@ -36,8 +36,8 @@ function wp_unslash($value) {
return $value;
}
function aksrt_test_apply_comments_clauses(array $clauses, WP_Comment_Query $query): array {
$filters = $GLOBALS['aksrt_test_filters']['comments_clauses'] ?? [];
function jio_test_apply_comments_clauses(array $clauses, WP_Comment_Query $query): array {
$filters = $GLOBALS['jio_test_filters']['comments_clauses'] ?? [];
usort($filters, static fn (array $a, array $b): int => $a[1] <=> $b[1]);
foreach ($filters as [$callback, $priority, $accepted_args]) {
$clauses = $callback($clauses, $query);
@@ -45,19 +45,19 @@ function aksrt_test_apply_comments_clauses(array $clauses, WP_Comment_Query $que
return $clauses;
}
require __DIR__ . '/../../inc/helpers/aksrt-comments.php';
require __DIR__ . '/../../inc/helpers/jio-comments.php';
$newest_args = aksrt_comments_template_query_args(['order' => 'ASC']);
$newest_args = jio_comments_template_query_args(['order' => 'ASC']);
if (($newest_args['order'] ?? '') !== 'DESC' || ($newest_args['orderby'] ?? '') !== 'comment_date_gmt') {
throw new RuntimeException('The newest tab must use descending comment date order regardless of the WordPress discussion setting.');
}
$_GET['aksrt_comment_order'] = 'hottest';
$hottest_args = aksrt_comments_template_query_args([]);
if (empty($hottest_args['aksrt_hottest_order']) || ($hottest_args['order'] ?? '') !== 'DESC') {
$_GET['jio_comment_order'] = 'hottest';
$hottest_args = jio_comments_template_query_args([]);
if (empty($hottest_args['jio_hottest_order']) || ($hottest_args['order'] ?? '') !== 'DESC') {
throw new RuntimeException('The hottest tab must set a descending, query-local hottest marker.');
}
unset($_GET['aksrt_comment_order']);
unset($_GET['jio_comment_order']);
$base = [
'join' => '',
@@ -65,15 +65,15 @@ $base = [
'orderby' => 'wp_comments.comment_date_gmt DESC',
];
$hot = aksrt_test_apply_comments_clauses($base, new WP_Comment_Query([
$hot = jio_test_apply_comments_clauses($base, new WP_Comment_Query([
'post_id' => 42,
'aksrt_hottest_order' => true,
'jio_hottest_order' => true,
]));
if (!str_contains($hot['join'], 'LEFT JOIN wp_commentmeta AS aksrt_cm_hot')) {
if (!str_contains($hot['join'], 'LEFT JOIN wp_commentmeta AS jio_cm_hot')) {
throw new RuntimeException('Hottest ordering must use LEFT JOIN so comments without like metadata remain in the result.');
}
if (!str_contains($hot['orderby'], 'COALESCE(aksrt_cm_hot.meta_value, 0) AS UNSIGNED) DESC')) {
if (!str_contains($hot['orderby'], 'COALESCE(jio_cm_hot.meta_value, 0) AS UNSIGNED) DESC')) {
throw new RuntimeException('Hottest ordering must sort likes from high to low and treat missing metadata as zero.');
}
if (!str_contains($hot['orderby'], 'wp_comments.comment_date_gmt DESC')) {
@@ -83,7 +83,7 @@ if ($hot['where'] !== $base['where']) {
throw new RuntimeException('Hottest sorting must not alter the query scope.');
}
$ordinary = aksrt_test_apply_comments_clauses($base, new WP_Comment_Query([
$ordinary = jio_test_apply_comments_clauses($base, new WP_Comment_Query([
'post_id' => 99,
'orderby' => 'comment_date_gmt',
]));
@@ -9,32 +9,32 @@ const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('comment order matches the reference newest and hottest behavior', () => {
const php = read('inc/helpers/aksrt-comments.php');
const php = read('inc/helpers/jio-comments.php');
assert.match(php, /\$args\['order'\] = 'DESC';\s*\$args\['orderby'\] = 'comment_date_gmt';/);
assert.match(php, /\$args\['aksrt_hottest_order'\] = true;/);
assert.match(php, /CAST\(COALESCE\(aksrt_cm_hot\.meta_value, 0\) AS UNSIGNED\) DESC/);
assert.match(php, /\$query->query_vars\['aksrt_hottest_order'\]/);
assert.match(php, /\$args\['jio_hottest_order'\] = true;/);
assert.match(php, /CAST\(COALESCE\(jio_cm_hot\.meta_value, 0\) AS UNSIGNED\) DESC/);
assert.match(php, /\$query->query_vars\['jio_hottest_order'\]/);
assert.equal(php.includes("$args['order'] = 'ASC'"), false, 'the reference always orders newest comments first');
assert.equal(php.includes("$order === 'oldest'"), false, 'the unsupported oldest tab must not remain in the UI query path');
assert.equal(php.includes('aksrt_comments_apply_hottest_order'), false, 'unscoped global hot-order hook must be removed');
assert.equal(php.includes('jio_comments_apply_hottest_order'), false, 'unscoped global hot-order hook must be removed');
assert.equal(php.includes('reverse_top_level'), false, 'DESC results must not be reversed a second time');
});
test('empty comment state uses the reference illustration and caption', () => {
const php = read('inc/helpers/aksrt-comments.php');
const php = read('inc/helpers/jio-comments.php');
assert.match(php, /aksrt_get_null\(\$text, 40, 'comment-null\.svg', 'comment comment-null', 280\)/);
assert.match(php, /\$text = __\('暂无评论内容', 'aksrt'\);/);
assert.match(php, /jio_get_null\(\$text, 40, 'comment-null\.svg', 'comment comment-null', 280\)/);
assert.match(php, /\$text = __\('暂无评论内容', 'jio'\);/);
assert.ok(fs.existsSync(path.join(root, 'assets/img/comment-null.svg')), 'the reference empty-state illustration must ship with the theme');
assert.equal(php.includes('暂无作者评论'), false, 'empty filter state uses the reference caption');
});
test('the shared empty illustration stays centered after the block-level media reset', () => {
assert.match(read('inc/helpers/aksrt-null.php'), /<img class="no-imgbox aksrt-null__img"/);
assert.match(read('inc/helpers/jio-null.php'), /<img class="no-imgbox jio-null__img"/);
for (const file of ['assets/css/frontend/empty-state.css', 'assets/css/build/public.css']) {
const css = read(file);
const rule = css.match(/\.aksrt-null\s*>\s*\.aksrt-null__img\s*\{([^}]+)\}/);
const rule = css.match(/\.jio-null\s*>\s*\.jio-null__img\s*\{([^}]+)\}/);
assert.ok(rule, `${file} must provide the shared illustration alignment`);
assert.match(rule[1], /display:\s*block;/);
assert.match(rule[1], /margin-right:\s*auto;/);
@@ -28,7 +28,7 @@ test('comment smilie assets and their runtime chain stay removed', () => {
'assets/js/runtime/imgbox.min.js',
'assets/js/runtime/main.js',
'assets/js/runtime/main.min.js',
'inc/helpers/aksrt-comments.php',
'inc/helpers/jio-comments.php',
'inc/helpers/comments/content.php',
'inc/helpers/comments/form-parts.php',
'src/Admin/Schema/SchemaPostList.php',
@@ -38,7 +38,7 @@ test('comment smilie assets and their runtime chain stay removed', () => {
const forbidden = [
'comment_smilie',
'commentSmiliesBase',
'aksrt_comment_smilie',
'jio_comment_smilie',
'assets/img/smilies',
'data-smilie',
'dropdown-smilie',
@@ -16,7 +16,7 @@ test('term aggregation renders the real SVG use element, total count, and config
const fixture = renderFixtures();
const card = fixture.aggregation;
assert.match(card, /<svg class="zibsvg-hot" viewbox="0 0 16 16" aria-hidden="true"><use href="#zibsvg-hot"><\/use><\/svg>/);
const sanitizer = read('inc/helpers/aksrt-graphic-card.php');
const sanitizer = read('inc/helpers/jio-graphic-card.php');
assert.match(sanitizer, /'svg'\s*=>\s*\['class'\s*=>\s*true,\s*'viewbox'\s*=>\s*true,\s*'aria-hidden'\s*=>\s*true\]/);
assert.match(sanitizer, /'use'\s*=>\s*\['href'\s*=>\s*true,\s*'xlink:href'\s*=>\s*true\]/);
assert.match(card, /7篇文章/);
@@ -66,11 +66,11 @@ test('category header matches first-page, later-page, and simple-title render co
assert.match(second, /search-trem="7"/);
const simple = fixture.term_header_simple;
assert.match(simple, /class="aksrt-panel relative"/);
assert.match(simple, /class="jio-panel relative"/);
assert.match(simple, /title-h-left/);
assert.match(simple, /共31篇/);
assert.match(simple, /简洁描述/);
assert.doesNotMatch(simple, /class="page-cover aksrt-panel|<img/);
assert.doesNotMatch(simple, /class="page-cover jio-panel|<img/);
});
test('homepage sorting retains distinct dropdown and inline option containers', () => {
@@ -97,9 +97,9 @@ test('number pagination renders a page trigger and tab panes distinguish loaded
});
test('archive filters use the shared ajax-next handler without a category REST/archive script path', () => {
const filters = read('inc/helpers/aksrt-archive-filters.php');
const filters = read('inc/helpers/jio-archive-filters.php');
const ajax = read('assets/js/core/ajax-list.js');
const archives = read('inc/helpers/aksrt-archive.php');
const archives = read('inc/helpers/jio-archive.php');
const globalController = read('src/Admin/GlobalController.php');
assert.match(filters, /class="ajax-option ajax-replace" win-ajax-replace="filter"/);
assert.match(filters, /class="ajax-next/);
@@ -76,16 +76,16 @@ function wp_kses($value, array $allowed): string {
function is_wp_error($value): bool { return $value instanceof WP_Error; }
function wp_is_mobile(): bool { return false; }
function wp_reset_postdata(): void { $GLOBALS['discovery_current_post'] = null; }
function aksrt_is_lazy(string $key, bool $default = false): bool { return false; }
function aksrt_get_lazy_attr(string $type, string $url, string $class = '', string $lazy = ''): string { return ' class="' . esc_attr($class) . '" src="' . esc_url($url) . '"'; }
function aksrt_format_number($number): string { return number_format((int) $number); }
function aksrt_renderer_svg_icon(string $name): string { return '<svg class="zibsvg-' . esc_attr($name) . '" viewbox="0 0 16 16" aria-hidden="true"><use href="#zibsvg-' . esc_attr($name) . '"></use></svg>'; }
function aksrt_renderer_collect_post_data(array $config = []): array {
function jio_is_lazy(string $key, bool $default = false): bool { return false; }
function jio_get_lazy_attr(string $type, string $url, string $class = '', string $lazy = ''): string { return ' class="' . esc_attr($class) . '" src="' . esc_url($url) . '"'; }
function jio_format_number($number): string { return number_format((int) $number); }
function jio_renderer_svg_icon(string $name): string { return '<svg class="zibsvg-' . esc_attr($name) . '" viewbox="0 0 16 16" aria-hidden="true"><use href="#zibsvg-' . esc_attr($name) . '"></use></svg>'; }
function jio_renderer_collect_post_data(array $config = []): array {
$post = $GLOBALS['discovery_current_post'];
return ['post_id' => $post->ID, 'title' => '文章 ' . $post->ID, 'subtitle' => '', 'permalink' => 'https://example.test/post/' . $post->ID, 'time_ago' => '2天前', 'views' => 42, 'comments' => 3];
}
function aksrt_get_post_favorites(int $id): int { return 5; }
function aksrt_get_post_likes(int $id): int { return 6; }
function jio_get_post_favorites(int $id): int { return 5; }
function jio_get_post_likes(int $id): int { return 6; }
function get_term(int $id, string $taxonomy = 'category') { return $GLOBALS['discovery_terms'][$id] ?? new WP_Error('missing'); }
function get_term_meta(int $id, string $key, bool $single = true): int { return $id === 7 ? 77 : 0; }
function wp_get_attachment_url(int $id): string { return $id === 77 ? 'https://example.test/category-cover.jpg' : ''; }
@@ -94,9 +94,9 @@ function get_the_post_thumbnail_url($id = null, string $size = 'post-thumbnail')
function get_term_link(WP_Term $term) { return 'https://example.test/category/' . $term->term_id; }
function current_user_can(string $capability): bool { return false; }
function get_edit_term_link(int $id, string $taxonomy): string { return ''; }
function aksrt_widget_str_cut(string $text, int $length): string { return $text; }
function aksrt_render_placeholder(string $style, int $count): string { return '<div data-placeholder="' . esc_attr($style) . ':' . $count . '"></div>'; }
function aksrt_render_post_list(array $args, WP_Query $query, bool $echo = true): string { return '<div class="rendered-post-count" data-count="' . count($query->posts) . '"></div>'; }
function jio_widget_str_cut(string $text, int $length): string { return $text; }
function jio_render_placeholder(string $style, int $count): string { return '<div data-placeholder="' . esc_attr($style) . ':' . $count . '"></div>'; }
function jio_render_post_list(array $args, WP_Query $query, bool $echo = true): string { return '<div class="rendered-post-count" data-count="' . count($query->posts) . '"></div>'; }
function get_terms(array $args) {
$ids = array_map('intval', (array) ($args['include'] ?? []));
if (($args['fields'] ?? '') === 'ids') return [];
@@ -107,8 +107,8 @@ function get_queried_object() { return $GLOBALS['discovery_queried_term']; }
function get_query_var(string $name, $default = '') { return $name === 'paged' ? $GLOBALS['discovery_paged'] : $default; }
function get_ancestors(int $id, string $taxonomy, string $resource_type): array { return []; }
function wp_parse_args_filter($args, array $defaults): array { return array_merge($defaults, $args); }
function aksrt_homepage_get_current_url(): string { return 'https://example.test/?paged=3'; }
function aksrt_homepage_url_del_paged(string $url): string { return preg_replace('/[?&]paged=\d+/', '', $url) ?: $url; }
function jio_homepage_get_current_url(): string { return 'https://example.test/?paged=3'; }
function jio_homepage_url_del_paged(string $url): string { return preg_replace('/[?&]paged=\d+/', '', $url) ?: $url; }
function add_query_arg($args, $url = false): string {
if (is_array($args)) return (string) $url . (str_contains((string) $url, '?') ? '&' : '?') . http_build_query($args);
return 'https://example.test/';
@@ -119,12 +119,12 @@ function get_template_directory_uri(): string { return 'https://example.test/the
function wp_unslash($value): string { return (string) $value; }
require dirname(__DIR__, 2) . '/vendor/autoload.php';
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-graphic-card.php';
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-widgets-markup.php';
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-homepage.php';
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-archive-filters.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-graphic-card.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-widgets-markup.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-homepage.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-archive-filters.php';
use AKSRT\WidgetSystem\Renderers\ContentRenderer;
use Jio\WidgetSystem\Renderers\ContentRenderer;
$aggregation = ContentRenderer::render('term_aggregation', [
'term_id' => [7, 8], 'pc_row' => 2, 'orderby' => 'modified', 'order' => 'desc', 'count' => 2,
@@ -132,14 +132,14 @@ $aggregation = ContentRenderer::render('term_aggregation', [
$tabs = ContentRenderer::render('tab_posts', [
'style' => 'list', 'count' => 2, 'paginate' => 'number',
'tabs' => [['title' => '技术', 'cat' => '7'], ['title' => '空栏目', 'cat' => '8']],
], ['widget_id' => 'aksrt_widget_tab_posts-4']);
$posts = ContentRenderer::render('posts', ['style' => 'list', 'count' => 2, 'paginate' => 'number'], ['widget_id' => 'aksrt_widget_posts-3']);
], ['widget_id' => 'jio_widget_tab_posts-4']);
$posts = ContentRenderer::render('posts', ['style' => 'list', 'count' => 2, 'paginate' => 'number'], ['widget_id' => 'jio_widget_posts-3']);
$filter_group = aksrt_archive_build_term_filter_group('category', [
$filter_group = jio_archive_build_term_filter_group('category', [
'lists' => [7], 'dropdown' => true, 'dropdown_lists' => [9],
], '分类');
ob_start();
aksrt_archive_render_filter_bar(['groups' => [$filter_group], 'aria_label' => '分类筛选']);
jio_archive_render_filter_bar(['groups' => [$filter_group], 'aria_label' => '分类筛选']);
$archive_filter_html = (string) ob_get_clean();
$GLOBALS['discovery_queried_term'] = $GLOBALS['discovery_terms'][7];
@@ -147,7 +147,7 @@ $GLOBALS['wp_query'] = new WP_Query();
$GLOBALS['wp_query']->found_posts = 31;
$GLOBALS['discovery_paged'] = 1;
ob_start();
aksrt_archive_render_term_header([
jio_archive_render_term_header([
'header_style' => 'cover', 'show_cover' => true, 'cover_url' => '',
'title' => '技术分类', 'description' => '首屏描述', 'post_count' => true,
]);
@@ -155,7 +155,7 @@ $term_header_first = (string) ob_get_clean();
$GLOBALS['discovery_paged'] = 2;
ob_start();
aksrt_archive_render_term_header([
jio_archive_render_term_header([
'header_style' => 'cover', 'show_cover' => true, 'cover_url' => '',
'title' => '技术分类', 'description' => '第二页描述', 'post_count' => true,
]);
@@ -163,13 +163,13 @@ $term_header_second = (string) ob_get_clean();
$GLOBALS['discovery_paged'] = 1;
ob_start();
aksrt_archive_render_term_header([
jio_archive_render_term_header([
'header_style' => 'simple', 'show_cover' => false,
'title' => '技术分类', 'description' => '简洁描述', 'post_count' => true,
]);
$term_header_simple = (string) ob_get_clean();
$home_sort = aksrt_homepage_get_option_orderby_but(['lists' => ['date', 'views'], 'dropdown' => true]);
$home_sort = jio_homepage_get_option_orderby_but(['lists' => ['date', 'views'], 'dropdown' => true]);
$result = json_encode([
'aggregation' => $aggregation,
@@ -17,21 +17,21 @@ test('the token relay is fully retired', () => {
assert.doesNotMatch(read('functions.php'), /Content\\Download'/);
// The orphaned cleanup cron is still unscheduled on theme switch.
assert.match(read('functions.php'), /aksrt_dl_token_cleanup/);
assert.match(read('functions.php'), /jio_dl_token_cleanup/);
// Header query-var branch and SEO noindex branch are gone with the route.
assert.doesNotMatch(read('inc/helpers/homepage/header-components.php'), /aksrt_dl_token/);
assert.doesNotMatch(read('inc/helpers/homepage/header-components.php'), /jio_dl_token/);
assert.doesNotMatch(read('src/Core/SEOHead.php'), /render_robots/);
assert.doesNotMatch(read('src/Core/SEOHead.php'), /aksrt_dl_token/);
assert.doesNotMatch(read('src/Core/SEOHead.php'), /jio_dl_token/);
});
test('the download box renders direct anchors with inline extract codes', () => {
const box = read('templates/parts/resource-download-box.php');
// No relay button, captcha surface or low-credit data attributes.
assert.doesNotMatch(box, /data-action="aksrt-download"/);
assert.doesNotMatch(box, /data-action="jio-download"/);
assert.doesNotMatch(box, /data-captcha/);
assert.doesNotMatch(box, /data-aksrt-download-captcha/);
assert.doesNotMatch(box, /data-jio-download-captcha/);
assert.doesNotMatch(box, /captcha_active/);
assert.doesNotMatch(box, /data-balance|data-cost/);
@@ -48,7 +48,7 @@ test('the download box renders direct anchors with inline extract codes', () =>
assert.match(box, /data-clipboard-tag="<\?php echo esc_attr\(\$has_copy \?/);
// Custom icon / label / color from the pay meta feed the anchor.
assert.match(box, /aksrt_icon_markup\(\$icon\)/);
assert.match(box, /jio_icon_markup\(\$icon\)/);
assert.match(box, /\$btn_text = \$label;/);
assert.match(box, /resource-download-box__btn--custom/);
@@ -65,10 +65,10 @@ test('the download box renders direct anchors with inline extract codes', () =>
// Hovering the remark shows 点击复制{copy name}, matching the reference theme,
// and only when a copy name exists.
assert.match(box, /\$has_copy && \$copy_key !== ''\) : \?>/);
assert.match(box, /sprintf\(__\('点击复制%s', 'aksrt'\), \$copy_key\)/);
assert.match(box, /sprintf\(__\('点击复制%s', 'jio'\), \$copy_key\)/);
assert.match(box, /data-toggle="tooltip"/);
const pay = read('inc/helpers/aksrt-pay.php');
assert.match(pay, /sprintf\(__\('点击复制%s', 'aksrt'\), \$copy_key\)/);
const pay = read('inc/helpers/jio-pay.php');
assert.match(pay, /sprintf\(__\('点击复制%s', 'jio'\), \$copy_key\)/);
// Bootstrap tooltips must be initialized for the attribute to do anything.
const main = read('assets/js/runtime/main.js');
assert.match(main, /jQuery\("\[data-toggle='tooltip'\]"\)\.tooltip\(/);
@@ -79,11 +79,11 @@ test('the download box renders direct anchors with inline extract codes', () =>
});
test('the paid card download actions render direct anchors', () => {
const pay = read('inc/helpers/aksrt-pay.php');
const pay = read('inc/helpers/jio-pay.php');
assert.doesNotMatch(pay, /data-action="aksrt-download"/);
assert.doesNotMatch(pay, /data-action="jio-download"/);
assert.doesNotMatch(pay, /data-captcha/);
assert.doesNotMatch(pay, /data-aksrt-download-captcha/);
assert.doesNotMatch(pay, /data-jio-download-captcha/);
assert.doesNotMatch(pay, /captcha_active/);
// Direct anchor to the raw source URL, empty sources skipped.
@@ -99,28 +99,28 @@ test('cms-download.js only serves the cashier now', () => {
// The download request flow is gone.
assert.doesNotMatch(js, /download\/request/);
assert.doesNotMatch(js, /data-action="aksrt-download"/);
assert.doesNotMatch(js, /data-action="jio-download"/);
assert.doesNotMatch(js, /renderSuccess|startDownload|performDownloadRequest|requestDownload/);
assert.doesNotMatch(js, /AKSRTCaptcha/);
assert.doesNotMatch(js, /JioCaptcha/);
// Cashier, scan-pay and error views remain.
assert.match(js, /data-aksrt-pay-content/);
assert.match(js, /window\.aksrtOpenCashier = function/);
assert.match(js, /data-jio-pay-content/);
assert.match(js, /window\.jioOpenCashier = function/);
assert.match(js, /function renderCashier/);
assert.match(js, /function renderScanPay/);
assert.match(js, /function renderError/);
// The script is still enqueued for resource / paid singles.
assert.match(read('src/Core/ScriptsPart1b.php'), /'aksrt-cms-download'/);
assert.match(read('src/Core/ScriptsPart1b.php'), /'jio-cms-download'/);
});
test('the admin download-logs page and its wiring are gone', () => {
// 下载记录菜单页面:类文件、菜单注册、init 接线、各入口链接全部移除。
assert.equal(exists('src/Admin/Native/PayDownloadLogs.php'), false);
assert.doesNotMatch(read('src/Admin/Native/PayMenus.php'), /download_logs|aksrt_download_logs|PayDownloadLogs/)
assert.doesNotMatch(read('src/Admin/Native/PayMenus.php'), /download_logs|jio_download_logs|PayDownloadLogs/)
assert.doesNotMatch(read('functions.php'), /PayDownloadLogs/)
assert.doesNotMatch(read('src/Admin/Native/PayProducts.php'), /aksrt_download_logs/)
assert.doesNotMatch(read('src/User/CreditsAdmin.php'), /aksrt_download_logs|下载记录/)
assert.doesNotMatch(read('src/Admin/Native/PayProducts.php'), /jio_download_logs/)
assert.doesNotMatch(read('src/User/CreditsAdmin.php'), /jio_download_logs|下载记录/)
})
test('download log retention settings no longer exist', () => {
@@ -133,11 +133,11 @@ test('download log retention settings no longer exist', () => {
test('downloads stay unrecorded', () => {
// 下载不做记录:没有任何 PHP 写入下载计数或下载令牌。
const php = [
'inc/helpers/aksrt-pay.php',
'inc/helpers/aksrt-resource.php',
'inc/helpers/jio-pay.php',
'inc/helpers/jio-resource.php',
'src/Content/PayRest.php',
'src/Content/Visibility.php',
].map(read).join('\n')
assert.doesNotMatch(php, /update_post_meta\([^)]*download_count/)
assert.doesNotMatch(php, /aksrt_update_post_counter\([^)]*download/)
assert.doesNotMatch(php, /jio_update_post_counter\([^)]*download/)
});
@@ -36,16 +36,16 @@ test('forum-only events stay out of the email catalog', () => {
})
test('email settings keep one canonical SMTP schema', () => {
assert.match(templates, /'option_key'\s*=>\s*'aksrt_settings'/)
assert.match(templates, /'option_key'\s*=>\s*'jio_settings'/)
assert.match(templates, /'mail_passwd'.*'secret'\s*=>\s*true/)
assert.match(templates, /'email_test_send'.*'ui_only'\s*=>\s*true/)
assert.match(mail, /Settings::get_all\(\)/)
assert.match(mail, /'auth'\s*=>\s*\$username\s*!==\s*''\s*&&\s*\$password\s*!==\s*''/)
assert.doesNotMatch(templates, /mail_smtpauth/)
assert.doesNotMatch(mail, /mail_smtpauth/)
assert.doesNotMatch(settings, /aksrt_mail_smtp/)
assert.doesNotMatch(globalController, /aksrt_mail_config|\/settings\/mail'/)
assert.doesNotMatch(userController, /aksrt_mail_templates|mail-templates/)
assert.doesNotMatch(settings, /jio_mail_smtp/)
assert.doesNotMatch(globalController, /jio_mail_config|\/settings\/mail'/)
assert.doesNotMatch(userController, /jio_mail_templates|mail-templates/)
})
test('email event schema exposes only notification switches', () => {
@@ -76,7 +76,7 @@ test('SMTP authentication and failures use the canonical mail service', () => {
assert.doesNotMatch(coreHooks, /\$phpmailer->setFrom\(/)
assert.match(mail, /add_action\('wp_mail_failed',\s*\$capture_failure\)/)
assert.match(mail, /remove_action\('wp_mail_failed',\s*\$capture_failure\)/)
assert.match(mail, /error_log\('\[AKSRT\] SMTP 测试失败:'/)
assert.match(mail, /error_log\('\[Jio\] SMTP 测试失败:'/)
assert.match(globalController, /详细原因已写入服务器错误日志/)
})
@@ -102,9 +102,9 @@ test('only the canonical mail service calls wp_mail', () => {
test('financial and account hooks are wired to the notification service', () => {
for (const hook of [
'aksrt_order_paid', 'aksrt_order_status_changed', 'aksrt_asset_transaction_committed',
'aksrt_withdrawal_submitted', 'aksrt_withdrawal_status_changed',
'aksrt_user_phone_changed',
'jio_order_paid', 'jio_order_status_changed', 'jio_asset_transaction_committed',
'jio_withdrawal_submitted', 'jio_withdrawal_status_changed',
'jio_user_phone_changed',
]) {
assert.match(notifications, new RegExp(`add_action\\('${hook}'`), hook)
}
@@ -8,10 +8,10 @@ const read = file => fs.readFileSync(path.join(root, file), 'utf8')
test('financial flow and withdrawal numbers use the new-only schema', () => {
const schema = read('src/Core/DBMigratorPart2.php')
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_financial_flow \([\s\S]*?UNIQUE KEY uniq_flow_number \(flow_number\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}jio_financial_flow \([\s\S]*?UNIQUE KEY uniq_flow_number \(flow_number\)/)
assert.match(schema, /UNIQUE KEY uniq_flow_operation \(operation_key\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_financial_flow_entry \([\s\S]*?UNIQUE KEY uniq_flow_line \(flow_id, line_no\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_withdrawal \([\s\S]*?withdrawal_number VARCHAR\(32\) NOT NULL[\s\S]*?UNIQUE KEY uniq_withdrawal_number \(withdrawal_number\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}jio_financial_flow_entry \([\s\S]*?UNIQUE KEY uniq_flow_line \(flow_id, line_no\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}jio_withdrawal \([\s\S]*?withdrawal_number VARCHAR\(32\) NOT NULL[\s\S]*?UNIQUE KEY uniq_withdrawal_number \(withdrawal_number\)/)
})
test('order, flow and withdrawal numbers share one prefix-free allocator', () => {
@@ -93,10 +93,10 @@ test('user withdrawal history uses one paginated ten-item endpoint', () => {
assert.match(controller, /'user_id'\s*=>\s*\$user_id/)
assert.doesNotMatch(renderer, /get_user_withdrawals\(/)
assert.doesNotMatch(renderer, /withdraw_history/)
assert.match(renderer, /data-aksrt-withdraw-history-list/)
assert.match(renderer, /data-jio-withdraw-history-list/)
assert.match(script, /\/earnings\/withdrawals\?page=/)
assert.match(script, /&per_page=10/)
assert.match(script, /data-aksrt-withdraw-history-page/)
assert.match(script, /data-jio-withdraw-history-page/)
assert.match(script, /实际到账:/)
assert.match(script, /元)/)
assert.match(script, /appendText\(row, text\(item && item\.created_at\)/)
@@ -136,12 +136,12 @@ test('order detail page drops tabs; ticket center replaces the after-sale submen
assert.ok(!page.includes('stat_card'), 'stat cards must be removed')
assert.ok(!repository.includes('get_admin_order_stats'), 'dead stats query must be removed')
// 售后整体移除:工单改走 users.php 的「工单管理」子菜单,支付中心不再承载售后
assert.ok(!menus.includes('aksrt_after_sales'), 'after-sales submenu slug must be gone')
assert.ok(!menus.includes('jio_after_sales'), 'after-sales submenu slug must be gone')
assert.ok(!page.includes('render_after_sales_admin_page'), 'after-sales admin page entry must be gone')
assert.ok(!page.includes('PAGE_AFTER_SALES_SLUG'), 'after-sales slug constant must be gone')
assert.ok(!page.includes('after_sale'), 'no after-sale handler may remain on the orders page')
const usersMenus = read('src/Admin/Native/UsersMenus.php')
assert.ok(usersMenus.includes("'aksrt_tickets'"), 'ticket submenu slug registered under users.php')
assert.ok(usersMenus.includes("'jio_tickets'"), 'ticket submenu slug registered under users.php')
assert.ok(usersMenus.includes('render_tickets_page'), 'ticket admin page entry registered')
const ticketTrait = read('src/Admin/Native/UsersMenusTicketsCenter.php')
assert.ok(ticketTrait.includes('use UsersMenusTicketsList;'), 'ticket list renderer trait composed')
@@ -155,11 +155,11 @@ test('order detail page drops tabs; ticket center replaces the after-sale submen
test('paid orders expose a refund button next to the detail link', () => {
const page = ['PayOrders', 'PayOrdersList', 'PayOrdersDetail'].map((f) => read(`src/Admin/Native/${f}.php`)).join('\n')
// 「标记退款」紧跟在「详情」右侧(行操作区由 order_actions_html 输出),仅已支付订单可见——已退款是已支付的唯一后继状态
assert.match(page, /详情', 'aksrt'\) \. '<\/a>'\s*\n\s*\. \$this->order_actions_html\(\$order_number, \$status\)/)
assert.match(page, /\/\/ 已支付是已退款的唯一前置状态。\s*\n\s*if \(\$status === DomainModel::ORDER_STATUS_PAID\) \{[\s\S]*?'aksrt_pay_refund_order'/,
assert.match(page, /详情', 'jio'\) \. '<\/a>'\s*\n\s*\. \$this->order_actions_html\(\$order_number, \$status\)/)
assert.match(page, /\/\/ 已支付是已退款的唯一前置状态。\s*\n\s*if \(\$status === DomainModel::ORDER_STATUS_PAID\) \{[\s\S]*?'jio_pay_refund_order'/,
'refund button only renders for paid orders')
assert.ok(page.includes('aksrt_pay_refund_order_'), 'per-order nonce covers the refund')
assert.ok(page.includes("add_action('admin_post_aksrt_pay_refund_order', [$this, 'handle_refund_order'])"),
assert.ok(page.includes('jio_pay_refund_order_'), 'per-order nonce covers the refund')
assert.ok(page.includes("add_action('admin_post_jio_pay_refund_order', [$this, 'handle_refund_order'])"),
'refund action registered')
assert.ok(page.includes('handle_refund_order'), 'refund handler exists')
// 退款闸门只剩「订单为已支付」:工单系统与订单退款已解耦,不再要求先有进行中的工单
@@ -198,7 +198,7 @@ test('caller-supplied order numbers are constrained to allocator shapes', () =>
// 不合规时必须回落到分配器,而不是原样写库。
assert.match(repository, /\$order_number = \$caller_supplied \? \$declared_number : \$this->allocate_order_number\(\);/)
// 弃用不受控号码时在 WP_DEBUG 下留痕。
assert.match(repository, /error_log\(sprintf\('\[AKSRT Order\] 弃用不合规的自带订单号:%s'/)
assert.match(repository, /error_log\(sprintf\('\[Jio Order\] 弃用不合规的自带订单号:%s'/)
})
test('order cleanup no longer guards on financial records', () => {
@@ -27,16 +27,16 @@ const runtimeFiles = [
test('front submit owns the typed media REST routes and media trait wiring', () => {
assert.match(frontSubmit, /use FrontSubmitMedia\s*;/);
assert.match(frontSubmit, /add_action\('rest_api_init', \[\$this, 'register_rest_routes'\]\)/);
assert.doesNotMatch(frontSubmit, /add_action\('aksrt_new_(?:add|edit)_posts', \[\$this, 'sync_post_attachments'\]\)/);
assert.doesNotMatch(frontSubmit, /add_action\('jio_new_(?:add|edit)_posts', \[\$this, 'sync_post_attachments'\]\)/);
assert.match(saveTrait, /\$this->sync_post_attachments\(\$post\);\s*if \(!\$pay_result\['success'\]\)/);
assert.match(frontSubmit, /register_rest_route\('aksrt\/v1', '\/front-submit\/media\/\(\?P<type>image\|video\|file\)'/);
assert.match(frontSubmit, /register_rest_route\('jio\/v1', '\/front-submit\/media\/\(\?P<type>image\|video\|file\)'/);
assert.match(frontSubmit, /'methods'\s*=>\s*\\WP_REST_Server::READABLE/);
assert.match(frontSubmit, /'callback'\s*=>\s*\[\$this, 'rest_list_attachments'\]/);
assert.match(frontSubmit, /'post_id'\s*=>\s*\[/);
assert.match(frontSubmit, /'permission_callback'\s*=>\s*\[\$this, 'can_list_attachments'\]/);
assert.match(frontSubmit, /register_rest_route\('aksrt\/v1', '\/front-submit\/upload\/\(\?P<kind>image\|video\|resource\)'/);
assert.match(frontSubmit, /register_rest_route\('jio\/v1', '\/front-submit\/upload\/\(\?P<kind>image\|video\|resource\)'/);
assert.match(frontSubmit, /'methods'\s*=>\s*'POST'/);
assert.match(frontSubmit, /'callback'\s*=>\s*\[\$this, 'rest_upload_attachment'\]/);
assert.match(frontSubmit, /'permission_callback'\s*=>\s*\[\$this, 'can_upload_attachment'\]/);
@@ -76,8 +76,8 @@ test('front submit media responses and uploads use the attachment contract', ()
assert.match(mediaTrait, /'filesizeInBytes'\s*=>/);
assert.match(frontSubmit, /\$files\['file'\]/);
assert.match(frontSubmit, /media_handle_upload\('aksrt_front_submit_file'/);
assert.match(frontSubmit, /update_post_meta\(\$attachment_id, '_aksrt_front_submit_media', \$kind\)/);
assert.match(frontSubmit, /media_handle_upload\('jio_front_submit_file'/);
assert.match(frontSubmit, /update_post_meta\(\$attachment_id, '_jio_front_submit_media', \$kind\)/);
assert.match(frontSubmit, /\$attachment\['success'\]\s*=\s*true/);
assert.match(editor, /body\.append\('file', file\)/);
assert.match(editor, /method:\s*'POST'/);
@@ -90,29 +90,29 @@ test('saved submission media sync enforces ownership and cleans stale parent/met
assert.match(mediaTrait, /\(int\) \$attachment->post_author !== \$user_id/);
assert.match(mediaTrait, /\['inherit', 'private'\]/);
assert.match(mediaTrait, /\(int\) \$attachment->post_parent !== 0/);
assert.match(mediaTrait, /'meta_key'\s*=>\s*'_aksrt_front_submit_media'/);
assert.match(mediaTrait, /delete_post_meta\(\$attachment_id, '_aksrt_front_submit_media'\)/);
assert.match(mediaTrait, /update_post_meta\(\$attachment_id, '_aksrt_front_submit_media', \$kind\)/);
assert.match(mediaTrait, /'meta_key'\s*=>\s*'_jio_front_submit_media'/);
assert.match(mediaTrait, /delete_post_meta\(\$attachment_id, '_jio_front_submit_media'\)/);
assert.match(mediaTrait, /update_post_meta\(\$attachment_id, '_jio_front_submit_media', \$kind\)/);
assert.match(mediaTrait, /featured_video_attachment_id/);
assert.match(mediaTrait, /data-aksrt-file-id/);
assert.match(mediaTrait, /data-jio-file-id/);
assert.match(mediaTrait, /\['post_parent'\s*=>\s*\$post_id\]/);
});
test('submit page configures media URLs, REST nonce, and the three media copy groups without mini-upload', () => {
assert.match(page, /'restNonce'\s*=>\s*wp_create_nonce\('wp_rest'\)/);
assert.match(page, /'mediaUrl'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media'\)\)/);
assert.match(page, /'image'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media\/image'\)\)\]/);
assert.match(page, /'video'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media\/video'\)\)\]/);
assert.match(page, /'file'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media\/file'\)\)\]/);
assert.match(page, /'mediaUrl'\s*=>\s*esc_url_raw\(rest_url\('jio\/v1\/front-submit\/media'\)\)/);
assert.match(page, /'image'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('jio\/v1\/front-submit\/media\/image'\)\)\]/);
assert.match(page, /'video'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('jio\/v1\/front-submit\/media\/video'\)\)\]/);
assert.match(page, /'file'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('jio\/v1\/front-submit\/media\/file'\)\)\]/);
for (const copy of ['mineImage', 'mineVideo', 'mineFile', 'uploadImage', 'uploadVideo', 'uploadFile']) {
assert.match(page, new RegExp(`['"]${copy}['"]`), `missing copy ${copy}`);
}
assert.doesNotMatch(page, /aksrt-mini-upload/);
assert.doesNotMatch(page, /jio-mini-upload/);
assert.equal(fs.existsSync(path.join(root, 'assets/js/mini-upload.js')), false, 'mini-upload.js must stay removed');
});
test('shared submit media picker exposes mine search paging selection and nonce-backed upload', () => {
assert.match(editor, /window\.AksrtSubmitMedia\s*=\s*\{\s*open:/);
assert.match(editor, /window\.JioSubmitMedia\s*=\s*\{\s*open:/);
assert.match(editor, /data-editor-media-tab="mine"/);
assert.match(editor, /data-editor-media-search/);
assert.match(editor, /state\.search\.length >= 2/);
@@ -126,13 +126,13 @@ test('shared submit media picker exposes mine search paging selection and nonce-
assert.match(editor, /if \(!multiple\) maxItems = 1/);
assert.match(editor, /state\.selected\.length >= maxItems/);
assert.match(editor, /headers:\s*\{\s*'X-WP-Nonce': cfg\.restNonce \|\| ''\s*\}/);
assert.match(editor, /data-aksrt-file-id/);
assert.match(editor, /data-jio-file-id/);
assert.doesNotMatch(editor, /data-download-file/);
assert.match(submit, /data-aksrt-submit-ready/);
assert.match(submit, /data-jio-submit-ready/);
});
test('paid downloads and featured covers use the shared picker and attachment IDs', () => {
assert.match(payDownload, /window\.AksrtSubmitMedia\.open\(/);
assert.match(payDownload, /window\.JioSubmitMedia\.open\(/);
assert.match(payDownload, /type:\s*'file'/);
assert.match(payDownload, /item\.attachment_id \|\| item\.id/);
assert.match(payDownload, /data-pay-resource-field="attachment_id"/);
@@ -140,7 +140,7 @@ test('paid downloads and featured covers use the shared picker and attachment ID
assert.match(domainModel, /max\(0, \(int\) \$row\['attachment_id'\]\)/);
assert.match(submit, /openMediaPicker:\s*function \(options\)/);
assert.match(submit, /window\.AksrtSubmitMedia\.open\(options\)/);
assert.match(submit, /window\.JioSubmitMedia\.open\(options\)/);
assert.match(submit, /type:\s*'image'/);
assert.match(submit, /type:\s*'video'/);
assert.match(submit, /item\.attachment_id \|\| item\.id/);
@@ -65,10 +65,10 @@ test('front submit uses the WordPress-managed jQuery runtime', () => {
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/);
assert.doesNotMatch(read('inc/modules/user-parts/urls.php'), /jio_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'\]/);
assert.match(page, /\['loader_js', 'jio-front-submit-editor', 'jio-front-submit-pay-download'\]/);
});
test('legacy runtime modules use jQuery explicitly without a dollar alias', () => {
@@ -96,8 +96,8 @@ test('theme runtime never changes native listener passivity for third-party edit
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(scripts, /enqueue_passive_polyfill|jio-passive-events/);
assert.doesNotMatch(scriptsPart2, /enqueue_passive_polyfill|jio-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);
@@ -137,6 +137,6 @@ test('front submit TinyMCE status bar shows one live character counter', () => {
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|字数:/);
assert.match(submitCss, /\.newposts-editor \.jio-submit-word-count\s*{[^}]*float:\s*right;/);
assert.doesNotMatch(legacyPageEdit, /initWordCount|updateWordCount|jio-page-edit__word-count|字数:/);
});
@@ -143,9 +143,9 @@ namespace {
function fp_attachment(int $id, int $author = 7, string $mime = 'image/jpeg', int $parent = 0, string $status = 'inherit', bool $pending = true, ?string $kind = 'image'): WP_Post {
$post = fp_post($id, 'attachment', $author, $status, '', $parent, $mime);
if ($kind !== null) update_post_meta($id, '_aksrt_front_submit_media', $kind);
if ($kind !== null) update_post_meta($id, '_jio_front_submit_media', $kind);
$GLOBALS['front_pending_state']['url_map']['https://example.test/uploads/media-' . $id . '.' . fp_ext($mime)] = $id;
if ($pending) \AKSRT\Attachment\PendingUploads::mark($id);
if ($pending) \Jio\Attachment\PendingUploads::mark($id);
return $post;
}
@@ -156,7 +156,7 @@ namespace {
}
function fp_pending(int $id): bool {
return get_post_meta($id, \AKSRT\Attachment\PendingUploads::META, true) !== '';
return get_post_meta($id, \Jio\Attachment\PendingUploads::META, true) !== '';
}
function fp_article_data(string $content, array $extra = []): array {
@@ -265,7 +265,7 @@ namespace {
}
return $id;
}
function aksrt_get_pay_meta($post_id): array {
function jio_get_pay_meta($post_id): array {
return $GLOBALS['front_pending_state']['pay_meta'][(int) $post_id] ?? [];
}
function attachment_url_to_postid($url): int {
@@ -343,10 +343,10 @@ namespace {
require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmit.php';
}
namespace AKSRT\Core {
namespace Jio\Core {
final class DomainModel {
public const PAY_META_KEY = '_aksrt_pay';
public const POST_META_KEY = 'aksrt_post_meta';
public const PAY_META_KEY = '_jio_pay';
public const POST_META_KEY = 'jio_post_meta';
}
final class FrontSubmitPendingProbe {
@@ -397,7 +397,7 @@ namespace AKSRT\Core {
}
}
namespace AKSRT\User {
namespace Jio\User {
final class Restriction {
private static ?self $instance = null;
public static function get_instance(): self { return self::$instance ??= new self(); }
@@ -407,10 +407,10 @@ namespace AKSRT\User {
}
namespace {
use AKSRT\Attachment\PendingUploads;
use AKSRT\Core\FrontSubmit;
use AKSRT\Core\FrontSubmitPendingProbe;
use AKSRT\Core\DomainModel;
use Jio\Attachment\PendingUploads;
use Jio\Core\FrontSubmit;
use Jio\Core\FrontSubmitPendingProbe;
use Jio\Core\DomainModel;
$submit = new FrontSubmitPendingProbe();
@@ -427,7 +427,7 @@ namespace {
fp_assert($response instanceof \WP_REST_Response && $response->get_status() === 201, 'real upload persistence should return a created response');
$id = (int) ($response->get_data()['attachment_id'] ?? 0);
fp_assert($id > 0 && fp_pending($id), 'each normalized upload must be marked pending');
fp_assert(($GLOBALS['front_pending_state']['post_meta'][$id]['_aksrt_front_submit_media'] ?? '') === 'resource', 'upload should retain the normalized media type');
fp_assert(($GLOBALS['front_pending_state']['post_meta'][$id]['_jio_front_submit_media'] ?? '') === 'resource', 'upload should retain the normalized media type');
}
fp_assert(count($GLOBALS['front_pending_state']['upload_calls']) === 2, 'single and sideload calls should each persist once');
fp_assert(empty($GLOBALS['front_pending_state']['upload_calls'][0]['overrides']['action']), 'ordinary upload should use the upload action');
@@ -477,7 +477,7 @@ namespace {
$content = 'Draft body with enough text for a save. '
. '<img data-edit-file-id="100" src="/image.jpg">'
. '<video data-edit-file-id="101" data-video-url="https://video.example.test/movie.mp4"></video>'
. '<a data-aksrt-file-id="102" href="/resource.pdf">resource</a>'
. '<a data-jio-file-id="102" href="/resource.pdf">resource</a>'
. implode(' ', $body_urls);
$result = $submit->persist(fp_article_data($content, [
'fake_saved_pay_meta' => ['downloads' => $pay_downloads],
@@ -534,7 +534,7 @@ namespace {
fp_assert(!empty($removed['success']), 'edit removing a media reference should save');
fp_assert(isset($GLOBALS['front_pending_state']['posts'][441]), 'unreferenced attached media must not be deleted');
fp_assert($GLOBALS['front_pending_state']['posts'][441]->post_parent === 0, 'removed media should be detached from the edited post');
fp_assert(get_post_meta(441, '_aksrt_front_submit_media', true) === '', 'removed media should lose only its submit relation marker');
fp_assert(get_post_meta(441, '_jio_front_submit_media', true) === '', 'removed media should lose only its submit relation marker');
fp_assert(!fp_pending(441) && $GLOBALS['front_pending_state']['deleted'] === [], 'removed media must not be deleted or marked pending again');
// A payment validation error happens after the body write. The saved body is
@@ -34,7 +34,7 @@ test('the global avatar shape primitive renders every avatar image circular', ()
assert.match(block, /object-fit:\s*cover/);
// The primitive must follow the existing avatar size system in the same file.
const sizeSystem = css.indexOf('.aksrt-avatar--xs {');
const sizeSystem = css.indexOf('.jio-avatar--xs {');
assert.ok(sizeSystem >= 0 && sizeSystem < start, 'primitive must sit after the avatar size system');
});
@@ -49,8 +49,8 @@ test('the user center and public user home keep their legacy rounded-square avat
// img and the upload overlay both inherit the frame radius.
assert.match(profile, /\.uc-header__avatar img\s*\{[^}]*border-radius:\s*inherit/s);
assert.match(userHome, /\.aksrt-author-hero__avatar img\s*\{[^}]*border-radius:\s*var\(--radius\)/s);
assert.doesNotMatch(userHome, /\.aksrt-author-hero__avatar img\s*\{[^}]*--radius-avatar/s);
assert.match(userHome, /\.jio-author-hero__avatar img\s*\{[^}]*border-radius:\s*var\(--radius\)/s);
assert.doesNotMatch(userHome, /\.jio-author-hero__avatar img\s*\{[^}]*--radius-avatar/s);
});
test('every other frontend avatar frame is circular', () => {
@@ -79,18 +79,18 @@ test('the VIP modal avatar carries the canonical avatar class', () => {
test('the VIP icon leaves the avatar corner and leads the badge row', () => {
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
const singleRender = read('inc/helpers/aksrt-single-render.php');
const singleRender = read('inc/helpers/jio-single-render.php');
const signin = read('src/Credits/Signin.php');
for (const [file, source] of [['UserRenderer.php', userRenderer], ['aksrt-single-render.php', singleRender]]) {
assert.match(source, /aksrt_get_avatar_box\([^)]*'avatar-img avatar-lg',\s*true,\s*false\)/, `${file} must drop the avatar corner badge`);
for (const [file, source] of [['UserRenderer.php', userRenderer], ['jio-single-render.php', singleRender]]) {
assert.match(source, /jio_get_avatar_box\([^)]*'avatar-img avatar-lg',\s*true,\s*false\)/, `${file} must drop the avatar corner badge`);
assert.match(source, /'vip'\s+=>\s+true/, `${file} must render the VIP icon in the name row`);
assert.doesNotMatch(source, /'vip'\s+=>\s+false/);
}
assert.match(signin, /aksrt_get_avatar_box\(\$user_id,\s*'avatar-img avatar-mini mr6',\s*false,\s*false\)/);
assert.match(signin, /aksrt_get_user_ban_name_badge\(\$user_id\)/);
assert.match(signin, /aksrt_get_vip_icon\(\$user_id\)/);
assert.match(signin, /jio_get_avatar_box\(\$user_id,\s*'avatar-img avatar-mini mr6',\s*false,\s*false\)/);
assert.match(signin, /jio_get_user_ban_name_badge\(\$user_id\)/);
assert.match(signin, /jio_get_vip_icon\(\$user_id\)/);
assert.match(signin, /\$avatar_img \. esc_html\(\$user->display_name\) \. \$name_badge/);
});
@@ -104,19 +104,19 @@ test('no frontend call site opts into the avatar corner VIP badge', () => {
if (entry.isDirectory()) { walk(full); continue; }
if (!/\.(php|js)$/.test(entry.name) || entry.name.endsWith('.min.js')) continue;
const source = fs.readFileSync(full, 'utf8');
const pattern = /aksrt_get_avatar_box\([^;]*?,\s*(?:true|false),\s*true\s*\)/g;
const pattern = /jio_get_avatar_box\([^;]*?,\s*(?:true|false),\s*true\s*\)/g;
for (const match of source.matchAll(pattern)) {
offenders.push(`${path.relative(root, full).split(path.sep).join('/')}: ${match[0]}`);
}
}
};
walk(root);
assert.deepEqual(offenders, [], 'aksrt_get_avatar_box() must no longer mount a badge on the avatar');
assert.deepEqual(offenders, [], 'jio_get_avatar_box() must no longer mount a badge on the avatar');
});
test('the avatar helper keeps its corner badge contract for callers that still want it', () => {
const avatar = read('inc/helpers/aksrt-avatar.php');
assert.match(avatar, /function aksrt_get_avatar_box\(\$id_or_email, string \$class = 'avatar-img', bool \$link = true, bool \$vip = true\)/);
const avatar = read('inc/helpers/jio-avatar.php');
assert.match(avatar, /function jio_get_avatar_box\(\$id_or_email, string \$class = 'avatar-img', bool \$link = true, bool \$vip = true\)/);
assert.match(avatar, /class="avatar-badge"/);
});
@@ -44,18 +44,18 @@ test('main owns the one global auto_fun boot', () => {
assert.match(main, /jQuery\(document\)\.trigger\(['"]auto_fun['"]\)/);
});
test('theme and comments do not depend on the removed aksrt namespace APIs', () => {
test('theme and comments do not depend on the removed jio namespace APIs', () => {
const theme = read('assets/js/core/theme.js');
const comments = read('assets/js/comment-ajax.js');
const runtime = read('assets/js/runtime/main.min.js');
assert.match(theme, /window\.aksrtData\.themeMode/);
assert.doesNotMatch(theme, /(?:window\.)?aksrt\.toggleTheme/);
assert.doesNotMatch(comments, /(?:window\.)?aksrt\.(?:__|confirm)/);
assert.doesNotMatch(runtime, /(?:window\.)?aksrt\.(?:toggleTheme|__|confirm)/);
assert.match(theme, /window\.jioData\.themeMode/);
assert.doesNotMatch(theme, /(?:window\.)?jio\.toggleTheme/);
assert.doesNotMatch(comments, /(?:window\.)?jio\.(?:__|confirm)/);
assert.doesNotMatch(runtime, /(?:window\.)?jio\.(?:toggleTheme|__|confirm)/);
});
test('theme initializes when the removed aksrt namespace is absent', () => {
test('theme initializes when the removed jio namespace is absent', () => {
const classList = { toggle() {}, contains() { return false; } };
const document = {
readyState: 'complete',
@@ -87,27 +87,27 @@ test('theme initializes when the removed aksrt namespace is absent', () => {
Date,
Math,
}));
assert.equal(window.aksrt, undefined);
assert.equal(window.jio, undefined);
});
test('routed modules use one namespaced auto_fun owner and no registry', () => {
for (const file of routed) {
const source = read(file);
assert.match(source, /\.off\(['"]auto_fun\.[^'"]+['"]\)\.on\(['"]auto_fun\.[^'"]+['"]/, file);
assert.doesNotMatch(source, /AksrtFrontend|aksrt\.autoFun|aksrt:(?:ready|rehydrated)/, file);
assert.doesNotMatch(source, /JioFrontend|jio\.autoFun|jio:(?:ready|rehydrated)/, file);
}
});
test('wallet reward content opens in the shared modal runtime once', () => {
const reward = read('assets/js/reward.js');
const helper = read('inc/helpers/aksrt-rewards.php');
const helper = read('inc/helpers/jio-rewards.php');
const authorBar = read('templates/parts/single-author-bar.php');
const singleActions = read('inc/helpers/aksrt-single-render.php');
const singleActions = read('inc/helpers/jio-single-render.php');
const scripts = read('src/Core/ScriptsPart1b.php');
const scriptsPart1c = read('src/Core/ScriptsPart1c.php');
assert.match(reward, /runtime\.open\(\{/);
assert.match(reward, /className: 'aksrt-reward-modal modal-mini full-sm'/);
assert.match(reward, /className: 'jio-reward-modal modal-mini full-sm'/);
assert.match(reward, /mobileBottom: true/);
assert.match(reward, /shell\.setAttribute\('aria-labelledby', title\.id\)/);
assert.match(reward, /'Idempotency-Key': key/);
@@ -119,31 +119,31 @@ test('wallet reward content opens in the shared modal runtime once', () => {
assert.match(helper, /static \$done = \[\]/);
assert.match(helper, /isset\(\$done\[\$key\]\)/);
assert.match(helper, /modal-colorful-header colorful-bg jb-blue/);
assert.match(helper, /data-aksrt-modal-close/);
assert.match(helper, /data-aksrt-reward-submit/);
assert.match(authorBar, /aksrt_render_rewards_template_for_user/);
assert.match(singleActions, /aksrt_render_rewards_template_for_user/);
assert.match(helper, /data-jio-modal-close/);
assert.match(helper, /data-jio-reward-submit/);
assert.match(authorBar, /jio_render_rewards_template_for_user/);
assert.match(singleActions, /jio_render_rewards_template_for_user/);
assert.match(scriptsPart1c, /\$requirements\['needs_reward'\]\s*=\s*\$is_article && !empty\(\$settings\['rewards_enabled'\]\)/);
assert.match(scriptsPart1c, /\$singular_assets\['needs_reward'\][\s\S]*?\$reward_js[\s\S]*?'aksrt-reward'/);
assert.doesNotMatch(helper + authorBar + singleActions, /aksrt_render_rewards_modal_for_user|reward-modal__qr/);
assert.match(scriptsPart1c, /\$singular_assets\['needs_reward'\][\s\S]*?\$reward_js[\s\S]*?'jio-reward'/);
assert.doesNotMatch(helper + authorBar + singleActions, /jio_render_rewards_modal_for_user|reward-modal__qr/);
});
test('user center navigation initializes once across auto_fun rescans', () => {
const source = read('assets/js/routes/user-center.js');
const initAt = source.indexOf('function initDashboardNavigation()');
const guardAt = source.indexOf('aksrtUserCenterNavigationInitialized', initAt);
const guardAt = source.indexOf('jioUserCenterNavigationInitialized', initAt);
const initialActivateAt = source.indexOf('activate(initialTab, false)', initAt);
assert.ok(initAt >= 0);
assert.ok(guardAt > initAt && guardAt < initialActivateAt);
assert.match(source.slice(initAt, initialActivateAt), /if \(document\.documentElement\.dataset\.aksrtUserCenterNavigationInitialized === 'true'\) return;/);
assert.match(source.slice(initAt, initialActivateAt), /document\.documentElement\.dataset\.aksrtUserCenterNavigationInitialized = 'true';/);
assert.match(source.slice(initAt, initialActivateAt), /if \(document\.documentElement\.dataset\.jioUserCenterNavigationInitialized === 'true'\) return;/);
assert.match(source.slice(initAt, initialActivateAt), /document\.documentElement\.dataset\.jioUserCenterNavigationInitialized = 'true';/);
assert.doesNotMatch(source, /name === 'vip' && !forceLoad\) forceLoad = true/);
});
test('homepage tabs refresh the original pane DOM through the runtime post_ajax pipeline', () => {
const layout = read('inc/helpers/homepage/layout.php')
const homepage = read('inc/helpers/aksrt-homepage.php')
const homepage = read('inc/helpers/jio-homepage.php')
const controller = read('src/Admin/GlobalController.php')
// legacy zib_index_tab_html 契约:data-toggle="tab" + data-ajax 栏目链接。
@@ -159,17 +159,17 @@ test('homepage tabs refresh the original pane DOM through the runtime post_ajax
assert.match(layout, /'nofilter' => 'true'/)
// 不再有旁路刷新:REST 面板、骨架屏、面板 footer 全部移除。
assert.equal(fs.existsSync(path.join(root, 'assets/js/homepage-fixed.js')), false)
assert.doesNotMatch(layout, /data-home-tab-panel|aksrt-homepage__panel|aksrt-homepage__tab|requestHomepagePanel|data-sticky-tabs/)
assert.doesNotMatch(layout, /data-home-tab-panel|jio-homepage__panel|jio-homepage__tab|requestHomepagePanel|data-sticky-tabs/)
assert.doesNotMatch(controller, /homepage\/tab-posts|rest_homepage_tab_posts/)
// 排序菜单 = zib_ajax_option_menu('home') 契约:win-ajax-replace="filter" + ajax-next 链接。
assert.match(homepage, /function aksrt_homepage_render_ajax_option_menu/)
assert.match(homepage, /function jio_homepage_render_ajax_option_menu/)
assert.match(homepage, /win-ajax-replace="filter"/)
assert.match(homepage, /ajax-replace="true" class="' \. esc_attr\(\$_class\)/)
assert.match(homepage, /!empty\(\$_GET\['nofilter'\]\)/)
// 主查询吃 ?orderby=(legacy zib_sift_posts_per_orde),排序原位刷新才有效。
assert.match(homepage, /function aksrt_homepage_get_request_sort/)
assert.match(read('inc/helpers/homepage/content.php'), /aksrt_homepage_get_request_sort\(\)/)
assert.match(homepage, /function jio_homepage_get_request_sort/)
assert.match(read('inc/helpers/homepage/content.php'), /jio_homepage_get_request_sort\(\)/)
// runtime 是 tab/排序的唯一引擎。
const runtime = read('assets/js/runtime/main.js')
@@ -180,7 +180,7 @@ test('homepage tabs refresh the original pane DOM through the runtime post_ajax
})
test('archive streams nest pagination inside the ajaxpager for tab extraction', () => {
const archive = read('inc/helpers/aksrt-archive.php')
const archive = read('inc/helpers/jio-archive.php')
assert.match(archive, /append_inner_html/)
assert.doesNotMatch(archive, /content-stream__footer/)
assert.match(read('inc/helpers/posts-renderer/markup.php'), /append_inner_html/)
@@ -219,6 +219,6 @@ test('non-generated first-party sources contain no deprecated lifecycle API', ()
});
for (const file of files) {
const source = fs.readFileSync(file, 'utf8');
assert.doesNotMatch(source, /AksrtFrontend|aksrt\.autoFun|aksrt:(?:ready|rehydrated)/, path.relative(root, file));
assert.doesNotMatch(source, /JioFrontend|jio\.autoFun|jio:(?:ready|rehydrated)/, path.relative(root, file));
}
});
@@ -6,22 +6,22 @@ declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
$GLOBALS['aksrt_set_settings'] = [];
$GLOBALS['aksrt_set_meta'] = [];
$GLOBALS['aksrt_set_json'] = null;
$GLOBALS['aksrt_set_output'] = '';
$GLOBALS['aksrt_set_term'] = null;
$GLOBALS['aksrt_set_post'] = null;
$GLOBALS['aksrt_set_queried'] = 0;
$GLOBALS['aksrt_set_post_type'] = 'post';
$GLOBALS['aksrt_set_is_single'] = false;
$GLOBALS['aksrt_set_is_page'] = false;
$GLOBALS['aksrt_set_is_category'] = false;
$GLOBALS['aksrt_set_object'] = null;
$GLOBALS['aksrt_set_page_templates'] = [];
$GLOBALS['aksrt_set_can'] = true;
$GLOBALS['aksrt_set_scripts'] = [];
$GLOBALS['aksrt_set_post_format'] = '';
$GLOBALS['jio_set_settings'] = [];
$GLOBALS['jio_set_meta'] = [];
$GLOBALS['jio_set_json'] = null;
$GLOBALS['jio_set_output'] = '';
$GLOBALS['jio_set_term'] = null;
$GLOBALS['jio_set_post'] = null;
$GLOBALS['jio_set_queried'] = 0;
$GLOBALS['jio_set_post_type'] = 'post';
$GLOBALS['jio_set_is_single'] = false;
$GLOBALS['jio_set_is_page'] = false;
$GLOBALS['jio_set_is_category'] = false;
$GLOBALS['jio_set_object'] = null;
$GLOBALS['jio_set_page_templates'] = [];
$GLOBALS['jio_set_can'] = true;
$GLOBALS['jio_set_scripts'] = [];
$GLOBALS['jio_set_post_format'] = '';
function __(string $text, string $domain = ''): string { return $text; }
function esc_html__($t, $d = ''): string { return (string) $t; }
@@ -35,22 +35,22 @@ namespace {
function wp_unslash($v) { return $v; }
function absint($v): int { return max(0, (int) $v); }
function is_user_logged_in(): bool { return true; }
function current_user_can($cap, ...$args): bool { return (bool) $GLOBALS['aksrt_set_can']; }
function is_single(): bool { return $GLOBALS['aksrt_set_is_single']; }
function is_page(): bool { return $GLOBALS['aksrt_set_is_page']; }
function is_category(): bool { return $GLOBALS['aksrt_set_is_category']; }
function current_user_can($cap, ...$args): bool { return (bool) $GLOBALS['jio_set_can']; }
function is_single(): bool { return $GLOBALS['jio_set_is_single']; }
function is_page(): bool { return $GLOBALS['jio_set_is_page']; }
function is_category(): bool { return $GLOBALS['jio_set_is_category']; }
function is_tag(): bool { return false; }
function is_tax(): bool { return false; }
function get_queried_object_id(): int { return $GLOBALS['aksrt_set_queried']; }
function get_queried_object() { return $GLOBALS['aksrt_set_object']; }
function get_post_type(): string { return $GLOBALS['aksrt_set_post_type']; }
function get_post($id) { return $GLOBALS['aksrt_set_post']; }
function get_post_meta($id, $key = '', $single = false) { return $GLOBALS['aksrt_set_meta'][$key] ?? ''; }
function update_post_meta($id, $key, $value): void { $GLOBALS['aksrt_set_meta'][$key] = $value; }
function get_queried_object_id(): int { return $GLOBALS['jio_set_queried']; }
function get_queried_object() { return $GLOBALS['jio_set_object']; }
function get_post_type(): string { return $GLOBALS['jio_set_post_type']; }
function get_post($id) { return $GLOBALS['jio_set_post']; }
function get_post_meta($id, $key = '', $single = false) { return $GLOBALS['jio_set_meta'][$key] ?? ''; }
function update_post_meta($id, $key, $value): void { $GLOBALS['jio_set_meta'][$key] = $value; }
function get_the_title($id): string { return '示例标题'; }
function get_the_permalink($id): string { return 'https://example.test/post'; }
function get_post_format($id) { return $GLOBALS['aksrt_set_post_format'] ?? ''; }
function set_post_format($id, $format): void { $GLOBALS['aksrt_set_meta']['__format'] = $format; }
function get_post_format($id) { return $GLOBALS['jio_set_post_format'] ?? ''; }
function set_post_format($id, $format): void { $GLOBALS['jio_set_meta']['__format'] = $format; }
function post_type_supports($type, $feature): bool { return $feature === 'post-formats'; }
function get_theme_support($feature) { return $feature === 'post-formats' ? [['image', 'video']] : false; }
function get_post_format_string($format): string { return ucfirst((string) $format); }
@@ -59,12 +59,12 @@ namespace {
function get_edit_term_link($id, $tax = '') { return 'https://example.test/wp-admin/term.php'; }
function get_taxonomy($tax) { return (object) ['labels' => (object) ['singular_name' => '分类']]; }
function get_post_type_object($type) { return (object) ['labels' => (object) ['singular_name' => $type === 'page' ? '页面' : '文章']]; }
function get_term($id, $tax = '') { return $GLOBALS['aksrt_set_term']; }
function wp_update_term($id, $tax, $args) { $GLOBALS['aksrt_set_meta']['__term'] = $args; return 1; }
function wp_update_post($args, $wp_error = false) { $GLOBALS['aksrt_set_meta']['__post'] = $args; return 1; }
function get_term($id, $tax = '') { return $GLOBALS['jio_set_term']; }
function wp_update_term($id, $tax, $args) { $GLOBALS['jio_set_meta']['__term'] = $args; return 1; }
function wp_update_post($args, $wp_error = false) { $GLOBALS['jio_set_meta']['__post'] = $args; return 1; }
function is_wp_error($v): bool { return false; }
function wp_is_mobile(): bool { return false; }
function is_page_template($tpl): bool { return in_array($tpl, $GLOBALS['aksrt_set_page_templates'], true); }
function is_page_template($tpl): bool { return in_array($tpl, $GLOBALS['jio_set_page_templates'], true); }
function home_url($path = ''): string { return 'https://example.test' . $path; }
function add_query_arg($args, $url = null) {
if ($args === null) { return 'https://example.test/current'; }
@@ -80,68 +80,68 @@ namespace {
}
function check_ajax_referer($action = -1, $name = '_wpnonce'): bool { return true; }
function wp_send_json($data, $code = null): void {
$GLOBALS['aksrt_set_json'] = $data;
$GLOBALS['jio_set_json'] = $data;
throw new \RuntimeException('__sent__');
}
function wp_kses($t, $allowed = null): string { return (string) $t; }
function checked($a, $b = true, $echo = true): string { return (string) $a === (string) $b ? ' checked="checked"' : ''; }
function selected($a, $b = true, $echo = true): string { return (string) $a === (string) $b ? ' selected="selected"' : ''; }
function wp_enqueue_script($handle, $src = '', $deps = [], $ver = false, $in_footer = false): void {
$GLOBALS['aksrt_set_scripts'][$handle] = ['src' => $src, 'deps' => $deps, 'in_footer' => $in_footer];
$GLOBALS['jio_set_scripts'][$handle] = ['src' => $src, 'deps' => $deps, 'in_footer' => $in_footer];
}
function get_template_directory(): string { return dirname(__DIR__, 2); }
function get_template_directory_uri(): string { return 'https://example.test/wp-content/themes/aksrt-theme'; }
function get_template_directory_uri(): string { return 'https://example.test/wp-content/themes/jio-theme'; }
function set_check(bool $condition, string $message): void {
if (!$condition) throw new \RuntimeException($message);
}
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class ExtensionRuntime {
public static array $settings = [];
public static function get_system_tools_settings(): array { return self::$settings; }
}
}
namespace AKSRT\Core {
namespace Jio\Core {
final class PostMetaBox {
public const META_KEY = 'aksrt_post_extension';
public static function get_meta(int $post_id): array { return $GLOBALS['aksrt_set_meta']; }
public const META_KEY = 'jio_post_extension';
public static function get_meta(int $post_id): array { return $GLOBALS['jio_set_meta']; }
}
final class DomainModel {
public const POST_VIEW_COUNT_KEY = 'aksrt_post_views';
public const POST_VIEW_COUNT_KEY = 'jio_post_views';
}
}
namespace {
use AKSRT\Frontend\PageAdminSet;
use Jio\Frontend\PageAdminSet;
require dirname(__DIR__, 2) . '/src/Frontend/PageAdminSet.php';
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
$GLOBALS['aksrt_set_queried'] = 0;
$GLOBALS['aksrt_set_is_single'] = true;
\Jio\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
$GLOBALS['jio_set_queried'] = 0;
$GLOBALS['jio_set_is_single'] = true;
// --- Gate: a non-admin-visible context must not render anything. ---
set_check(PageAdminSet::enabled() === false, 'enabled() must be false without a queried object');
$GLOBALS['aksrt_set_queried'] = 12;
$GLOBALS['jio_set_queried'] = 12;
set_check(PageAdminSet::enabled() === true, 'enabled() must be true on a single post for an admin');
$GLOBALS['aksrt_set_can'] = false;
$GLOBALS['jio_set_can'] = false;
set_check(PageAdminSet::enabled() === false, 'enabled() must respect manage_options');
$GLOBALS['aksrt_set_can'] = true;
$GLOBALS['jio_set_can'] = true;
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
\Jio\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
set_check(PageAdminSet::enabled() === false, 'enabled() must respect the admin_frontend_set switch');
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
\Jio\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
$GLOBALS['aksrt_set_page_templates'] = ['templates/pages/page-submit.php'];
$GLOBALS['jio_set_page_templates'] = ['templates/pages/page-submit.php'];
set_check(PageAdminSet::enabled() === false, 'enabled() must exclude the submit page template');
$GLOBALS['aksrt_set_page_templates'] = [];
$GLOBALS['jio_set_page_templates'] = [];
$GLOBALS['aksrt_set_post'] = (object) ['ID' => 12, 'post_type' => 'post'];
$GLOBALS['jio_set_post'] = (object) ['ID' => 12, 'post_type' => 'post'];
// --- Modal: reference markup parity. ---
ob_start();
@@ -154,7 +154,7 @@ namespace {
set_check(strpos($modal, 'class="modal-content page-set-modal"') !== false, 'modal must keep the page-set-modal class');
set_check(strpos($modal, 'class="modal-dialog" role="document"') !== false, 'modal must keep the reference dialog/document roles');
set_check(strpos($modal, 'data-dismiss="modal"') !== false, 'modal header must expose the dismiss button');
set_check(strpos($modal, 'name="action" value="aksrt_frontend_set_save"') !== false, 'modal must post aksrt_frontend_set_save');
set_check(strpos($modal, 'name="action" value="jio_frontend_set_save"') !== false, 'modal must post jio_frontend_set_save');
set_check(strpos($modal, 'name="taxonomy" value=""') !== false, 'modal must post the taxonomy field');
set_check(strpos($modal, 'name="post_type" value="post"') !== false, 'modal must post the post_type field');
set_check(strpos($modal, 'name="id" value="12"') !== false, 'modal must post the object id');
@@ -188,53 +188,53 @@ namespace {
set_check($e->getMessage() === '__sent__', 'ajax_save must terminate via wp_send_json');
}
set_check(is_array($GLOBALS['aksrt_set_json']), 'save must send a JSON payload');
set_check(($GLOBALS['aksrt_set_json']['msg'] ?? '') === '保存成功,正在刷新页面', 'save must reuse the reference success message');
set_check(!empty($GLOBALS['aksrt_set_json']['reload']), 'save must ask the client to reload');
set_check(($GLOBALS['aksrt_set_meta']['__post']['post_title'] ?? '') === '新标题', 'save must update the post title');
set_check(($GLOBALS['aksrt_set_meta']['__post']['comment_status'] ?? '') === 'closed', 'save must close comments when empty');
set_check(is_array($GLOBALS['jio_set_json']), 'save must send a JSON payload');
set_check(($GLOBALS['jio_set_json']['msg'] ?? '') === '保存成功,正在刷新页面', 'save must reuse the reference success message');
set_check(!empty($GLOBALS['jio_set_json']['reload']), 'save must ask the client to reload');
set_check(($GLOBALS['jio_set_meta']['__post']['post_title'] ?? '') === '新标题', 'save must update the post title');
set_check(($GLOBALS['jio_set_meta']['__post']['comment_status'] ?? '') === 'closed', 'save must close comments when empty');
$saved_meta = $GLOBALS['aksrt_set_meta']['aksrt_post_extension'] ?? [];
$saved_meta = $GLOBALS['jio_set_meta']['jio_post_extension'] ?? [];
set_check(($saved_meta['show_layout'] ?? '') === 'no_sidebar', 'save must persist show_layout');
set_check(($saved_meta['subtitle'] ?? '') === '副标题', 'save must persist the subtitle');
set_check(!empty($saved_meta['no_article-navs']), 'checked checkbox must persist as on');
set_check(empty($saved_meta['article_maxheight_xz']), 'unchecked checkbox must persist as off');
set_check((int) $GLOBALS['aksrt_set_meta']['aksrt_post_views'] === 42, 'save must persist the view count');
set_check((int) $GLOBALS['aksrt_set_meta']['_aksrt_like_count'] === 7, 'save must persist the like count');
set_check((int) $GLOBALS['jio_set_meta']['jio_post_views'] === 42, 'save must persist the view count');
set_check((int) $GLOBALS['jio_set_meta']['_jio_like_count'] === 7, 'save must persist the like count');
// --- Permission: the switch off must reject the save even with a valid object. ---
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
$GLOBALS['aksrt_set_json'] = null;
\Jio\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
$GLOBALS['jio_set_json'] = null;
try {
(new PageAdminSet())->ajax_save();
} catch (\RuntimeException $e) {
// expected
}
set_check(($GLOBALS['aksrt_set_json']['error'] ?? 0) === 1, 'disabled switch must reject the save');
set_check(!empty($GLOBALS['aksrt_set_json']['ys']), 'rejection must carry the danger type for the legacy client');
set_check(($GLOBALS['jio_set_json']['error'] ?? 0) === 1, 'disabled switch must reject the save');
set_check(!empty($GLOBALS['jio_set_json']['ys']), 'rejection must carry the danger type for the legacy client');
// --- Term save path. ---
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
$GLOBALS['aksrt_set_json'] = null;
$GLOBALS['aksrt_set_meta'] = [];
$GLOBALS['aksrt_set_term'] = (object) ['term_id' => 5, 'taxonomy' => 'category', 'name' => '旧名称', 'description' => ''];
\Jio\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
$GLOBALS['jio_set_json'] = null;
$GLOBALS['jio_set_meta'] = [];
$GLOBALS['jio_set_term'] = (object) ['term_id' => 5, 'taxonomy' => 'category', 'name' => '旧名称', 'description' => ''];
$_POST = ['id' => '5', 'type' => 'tax', 'taxonomy' => 'category', 'term_name' => '新分类', 'term_description' => '描述'];
try {
(new PageAdminSet())->ajax_save();
} catch (\RuntimeException $e) {
// expected
}
set_check(($GLOBALS['aksrt_set_meta']['__term']['name'] ?? '') === '新分类', 'term save must update the name');
set_check(($GLOBALS['aksrt_set_meta']['__term']['description'] ?? '') === '描述', 'term save must update the description');
set_check(($GLOBALS['jio_set_meta']['__term']['name'] ?? '') === '新分类', 'term save must update the name');
set_check(($GLOBALS['jio_set_meta']['__term']['description'] ?? '') === '描述', 'term save must update the description');
// --- Page branch: 标题样式 + 模块布局 + 容器位置. ---
$GLOBALS['aksrt_set_queried'] = 21;
$GLOBALS['aksrt_set_is_single'] = false;
$GLOBALS['aksrt_set_is_page'] = true;
$GLOBALS['aksrt_set_post_type'] = 'page';
$GLOBALS['aksrt_set_post'] = (object) ['ID' => 21, 'post_type' => 'page'];
$GLOBALS['aksrt_set_json'] = null;
$GLOBALS['aksrt_set_meta'] = [];
$GLOBALS['jio_set_queried'] = 21;
$GLOBALS['jio_set_is_single'] = false;
$GLOBALS['jio_set_is_page'] = true;
$GLOBALS['jio_set_post_type'] = 'page';
$GLOBALS['jio_set_post'] = (object) ['ID' => 21, 'post_type' => 'page'];
$GLOBALS['jio_set_json'] = null;
$GLOBALS['jio_set_meta'] = [];
$_POST = [
'id' => '21',
'type' => 'post',
@@ -248,41 +248,41 @@ namespace {
} catch (\RuntimeException $e) {
// expected
}
set_check(($GLOBALS['aksrt_set_meta']['page_header_style'] ?? '') === '2', 'page save must persist the header style');
set_check(($GLOBALS['aksrt_set_meta']['widgets_register'] ?? '') === '1', 'page save must enable the widget container');
set_check(($GLOBALS['aksrt_set_meta']['widgets_register_container'] ?? []) === ['sidebar', 'top_fluid'], 'page save must persist container positions');
set_check(($GLOBALS['jio_set_meta']['page_header_style'] ?? '') === '2', 'page save must persist the header style');
set_check(($GLOBALS['jio_set_meta']['widgets_register'] ?? '') === '1', 'page save must enable the widget container');
set_check(($GLOBALS['jio_set_meta']['widgets_register_container'] ?? []) === ['sidebar', 'top_fluid'], 'page save must persist container positions');
$GLOBALS['aksrt_set_json'] = null;
$GLOBALS['aksrt_set_meta'] = [];
$GLOBALS['jio_set_json'] = null;
$GLOBALS['jio_set_meta'] = [];
$_POST = ['id' => '21', 'type' => 'post', 'post_title' => '页面标题', 'widgets_register' => '0'];
try {
(new PageAdminSet())->ajax_save();
} catch (\RuntimeException $e) {
// expected
}
set_check(($GLOBALS['aksrt_set_meta']['widgets_register'] ?? '') === '', 'closing the widget container must clear the flag');
set_check(($GLOBALS['aksrt_set_meta']['widgets_register_container'] ?? ['x']) === [], 'closing the widget container must clear positions');
set_check(($GLOBALS['jio_set_meta']['widgets_register'] ?? '') === '', 'closing the widget container must clear the flag');
set_check(($GLOBALS['jio_set_meta']['widgets_register_container'] ?? ['x']) === [], 'closing the widget container must clear positions');
// --- Hint script: the transient label must fade like the reference theme. ---
PageAdminSet::enqueue_hint_script();
$hint = $GLOBALS['aksrt_set_scripts']['aksrt-front-page-admin-set'] ?? null;
$hint = $GLOBALS['jio_set_scripts']['jio-front-page-admin-set'] ?? null;
set_check(is_array($hint), 'the hint fade script must be enqueued when the feature is on');
set_check(strpos((string) ($hint['src'] ?? ''), '/assets/js/front-page-admin-set.js') !== false, 'the hint fade script must point at front-page-admin-set.js');
set_check(($hint['deps'] ?? []) === ['jquery'], 'the hint fade script must depend on jQuery');
set_check(!empty($hint['in_footer']), 'the hint fade script must load in the footer');
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
$GLOBALS['aksrt_set_scripts'] = [];
\Jio\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
$GLOBALS['jio_set_scripts'] = [];
PageAdminSet::enqueue_hint_script();
set_check($GLOBALS['aksrt_set_scripts'] === [], 'the disabled switch must not enqueue the hint fade script');
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
set_check($GLOBALS['jio_set_scripts'] === [], 'the disabled switch must not enqueue the hint fade script');
\Jio\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
// --- Icons: the site-wide convention is the normalized `fa fas fa-*` form. ---
$GLOBALS['aksrt_set_queried'] = 12;
$GLOBALS['aksrt_set_is_single'] = true;
$GLOBALS['aksrt_set_is_page'] = false;
$GLOBALS['aksrt_set_post_type'] = 'post';
$GLOBALS['aksrt_set_post'] = (object) ['ID' => 12, 'post_type' => 'post'];
$GLOBALS['jio_set_queried'] = 12;
$GLOBALS['jio_set_is_single'] = true;
$GLOBALS['jio_set_is_page'] = false;
$GLOBALS['jio_set_post_type'] = 'post';
$GLOBALS['jio_set_post'] = (object) ['ID' => 12, 'post_type' => 'post'];
ob_start();
(new PageAdminSet())->render_modal();
$modal_icons = (string) ob_get_clean();
@@ -310,8 +310,8 @@ namespace {
set_check(strpos($modal_icons, "value=\"{$extra}\"") === false, "post format options must not leak the theme-only format {$extra}");
}
// 已设置格式时必须回显选中项。
$GLOBALS['aksrt_set_meta']['__format'] = '';
$GLOBALS['aksrt_set_post_format'] = 'image';
$GLOBALS['jio_set_meta']['__format'] = '';
$GLOBALS['jio_set_post_format'] = 'image';
ob_start();
(new PageAdminSet())->render_modal();
$modal_image = (string) ob_get_clean();
@@ -1,13 +1,13 @@
<?php
/** Render-level harness for the header multifunction component. */
namespace AKSRT\Core {
namespace Jio\Core {
final class Color {
public static function sanitize(string $value): string { return $value; }
}
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static array $headerSlider = [];
public static function get_header_slider_settings(): array { return self::$headerSlider; }
@@ -16,8 +16,8 @@ namespace AKSRT\Settings {
namespace {
define('ABSPATH', __DIR__);
$GLOBALS['aksrt_mobile'] = false;
$GLOBALS['aksrt_lazy'] = true;
$GLOBALS['jio_mobile'] = false;
$GLOBALS['jio_lazy'] = true;
final class WP_Query {
public function get(string $key) { return 0; }
@@ -25,7 +25,7 @@ namespace {
$GLOBALS['wp_query'] = new WP_Query();
function is_admin(): bool { return false; }
function wp_is_mobile(): bool { return !empty($GLOBALS['aksrt_mobile']); }
function wp_is_mobile(): bool { return !empty($GLOBALS['jio_mobile']); }
function is_author(): bool { return false; }
function is_front_page(): bool { return true; }
function is_category(): bool { return false; }
@@ -44,19 +44,19 @@ namespace {
function esc_html($value): string { return htmlspecialchars((string) $value, ENT_QUOTES); }
function wp_strip_all_tags($value): string { return strip_tags((string) $value); }
function __($value, $domain = ''): string { return (string) $value; }
function get_bloginfo($show = ''): string { return 'AKSRT'; }
function get_bloginfo($show = ''): string { return 'Jio'; }
function home_url($path = ''): string { return 'https://example.test/' . ltrim((string) $path, '/'); }
function wp_kses($value, $allowed = []): string { return (string) $value; }
function aksrt_is_lazy(string $key): bool { return !empty($GLOBALS['aksrt_lazy']); }
function aksrt_get_lazy_thumb(string $size = ''): string { return 'https://example.test/placeholder.svg'; }
function aksrt_get_lazy_attr(string $key, string $src, string $class = '', string $lazy_src = ''): string {
if (aksrt_is_lazy($key)) {
function jio_is_lazy(string $key): bool { return !empty($GLOBALS['jio_lazy']); }
function jio_get_lazy_thumb(string $size = ''): string { return 'https://example.test/placeholder.svg'; }
function jio_get_lazy_attr(string $key, string $src, string $class = '', string $lazy_src = ''): string {
if (jio_is_lazy($key)) {
return ' class="lazyload ' . esc_attr($class) . '" src="' . esc_url($lazy_src) . '" data-src="' . esc_url($src) . '"';
}
return ' class="' . esc_attr($class) . '" src="' . esc_url($src) . '"';
}
function aksrt_get_search_box(array $args = []): string { return '<div class="search-input">search-box</div>'; }
function aksrt_icon_cover_card(array $card): string {
function jio_get_search_box(array $args = []): string { return '<div class="search-input">search-box</div>'; }
function jio_icon_cover_card(array $card): string {
return '<div class="icon-cover-card flex ac ' . esc_attr((string) ($card['class'] ?? '')) . '">' . esc_html((string) ($card['title'] ?? '')) . '</div>';
}
@@ -81,30 +81,30 @@ namespace {
'card_config' => ['show_widget_bg' => false, 'cards' => [['icon' => 'fa fas fa-star', 'title' => 'Card one']]],
'filter_blur' => ['pc_s', 'm_s'],
];
\AKSRT\Settings\Settings::$headerSlider = $settings;
\Jio\Settings\Settings::$headerSlider = $settings;
ob_start();
aksrt_header_slide();
jio_header_slide();
$desktop = (string) ob_get_clean();
$GLOBALS['aksrt_mobile'] = true;
$GLOBALS['jio_mobile'] = true;
ob_start();
aksrt_header_slide();
jio_header_slide();
$mobile = (string) ob_get_clean();
$settings['card_config']['show_widget_bg'] = true;
\AKSRT\Settings\Settings::$headerSlider = $settings;
\Jio\Settings\Settings::$headerSlider = $settings;
ob_start();
aksrt_header_slide();
jio_header_slide();
$module_background = (string) ob_get_clean();
$GLOBALS['aksrt_mobile'] = false;
$GLOBALS['aksrt_lazy'] = false;
$lazy_disabled = aksrt_get_header_slider_markup([
$GLOBALS['jio_mobile'] = false;
$GLOBALS['jio_lazy'] = false;
$lazy_disabled = jio_get_header_slider_markup([
'slides' => [['background' => 'https://example.test/eager.jpg']],
'class' => 'slide-index slide-header',
]);
$single_slide = aksrt_get_header_slider_markup([
$single_slide = jio_get_header_slider_markup([
'slides' => [['background' => 'https://example.test/single.jpg']],
'class' => 'slide-index slide-header',
'pagination' => true,
@@ -112,7 +112,7 @@ namespace {
]);
require_once dirname(__DIR__, 2) . '/src/Admin/PageConfigMetaBox.php';
$sanitize_cards = new \ReflectionMethod('AKSRT\\Admin\\PageConfigMetaBox', 'sanitize_card_options');
$sanitize_cards = new \ReflectionMethod('Jio\\Admin\\PageConfigMetaBox', 'sanitize_card_options');
$page_config_save = $sanitize_cards->invoke(null, ['show_widget_bg' => true, 'cards' => []]);
echo json_encode([
@@ -28,7 +28,7 @@ test('header multifunction renders reference slide, search, and card contracts o
assert.doesNotMatch(desktop, /<video controls/);
assert.match(desktop, /header-slider-search-more text-center before/);
assert.match(desktop, /<div class="search-input">search-box<\/div>/);
assert.match(desktop, /<div class="icon-cover-card flex ac aksrt-panel">Card one<\/div>/);
assert.match(desktop, /<div class="icon-cover-card flex ac jio-panel">Card one<\/div>/);
assert.doesNotMatch(mobile, /background\.mp4/);
assert.match(mobile, /data-src="https:\/\/example\.test\/mobile-only\.jpg"/);
@@ -36,9 +36,9 @@ test('header multifunction renders reference slide, search, and card contracts o
assert.match(mobile, /data-src="https:\/\/example\.test\/poster\.jpg"/);
assert.match(mobile, /class="new-dplayer" video-url="https:\/\/example\.test\/content\.mp4"/);
assert.match(mobile, /header-slider-search-more text-center before/);
assert.match(mobile, /<div class="icon-cover-card flex ac aksrt-panel">Card one<\/div>/);
assert.match(mobile, /<div class="icon-cover-card flex ac jio-panel">Card one<\/div>/);
assert.match(moduleBackground, /<div class="aksrt-panel padding-10"><div class="flex jse/);
assert.match(moduleBackground, /<div class="jio-panel padding-10"><div class="flex jse/);
assert.match(moduleBackground, /<div class="icon-cover-card flex ac padding-10">Card one<\/div>/);
assert.equal(JSON.parse(rendered.stdout).page_config_save.show_widget_bg, true);
+17 -17
View File
@@ -9,7 +9,7 @@ const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('icon card renderers keep vertical and horizontal legacy DOM contracts separate', () => {
const helper = read('inc/helpers/aksrt-icon-cards.php');
const helper = read('inc/helpers/jio-icon-cards.php');
const widgets = read('src/WidgetSystem/Renderers/GeneralRenderer.php');
const header = read('inc/helpers/homepage/header-components.php');
const mediaBrand = read('inc/helpers/template-tags/media-brand.php');
@@ -18,13 +18,13 @@ test('icon card renderers keep vertical and horizontal legacy DOM contracts sepa
assert.match(helper, /<div class="icon-cover-card flex ac /);
assert.match(helper, /icon-cover-icon badg cir/);
assert.match(helper, /icon-cover-desc ml10 flex1 px12-sm/);
assert.doesNotMatch(helper, /aksrt-icon-cover-card/);
assert.doesNotMatch(helper, /jio-icon-cover-card/);
assert.match(widgets, /'customize_icon' => \$card\['customize_icon'\] \?\? ''/);
assert.doesNotMatch(widgets, /\$card\['custom_icon'\]/);
// combo_info legitimately renders icon_image cards; icon cards must not leak media-card fields.
assert.doesNotMatch(widgets, /\$card\['description'\]/);
assert.match(header, /empty\(\$card\['custom_icon'\]\)/);
assert.doesNotMatch(mediaBrand, /function aksrt_render_icon_cover_card/);
assert.doesNotMatch(mediaBrand, /function jio_render_icon_cover_card/);
});
test('homepage horizontal cards render fully from legacy CSS with no dedicated stylesheet', () => {
@@ -35,7 +35,7 @@ test('homepage horizontal cards render fully from legacy CSS with no dedicated s
const scripts = read('src/Core/ScriptsPart1b.php');
assert.equal(fs.existsSync(path.join(root, 'assets/css/frontend/header-cards.css')), false);
assert.doesNotMatch(baseCss, /\.icon-cover-card\.aksrt-icon-cover-card/);
assert.doesNotMatch(baseCss, /\.icon-cover-card\.jio-icon-cover-card/);
assert.doesNotMatch(baseCss, /\.header-components-card/);
assert.doesNotMatch(baseCss, /\.header-slider-card\s+\.icon-cover-card\s*\{[^}]*font-size:/s);
assert.doesNotMatch(legacyUtilities, /\.px12-sm\s*\{[^}]*font-size:\s*12px/);
@@ -45,7 +45,7 @@ test('homepage horizontal cards render fully from legacy CSS with no dedicated s
assert.match(mainCss.slice(mobileBreakpoint, mobileTextSize + 120), /max-width:\s*640px/);
assert.match(mainCss.slice(mobileTextSize, mobileTextSize + 120), /font-size:\s*12px/);
assert.doesNotMatch(scripts, /header-cards/);
assert.doesNotMatch(widgetCss, /aksrt-widget-icon-cards \.icon-cover-card/);
assert.doesNotMatch(widgetCss, /jio-widget-icon-cards \.icon-cover-card/);
});
test('widget icon cards restore the reference surface height without compact overrides', () => {
@@ -78,22 +78,22 @@ test('header icon-card defaults use bundled Font Awesome icons', () => {
});
test('icon card renderers route every picker format through the shared icon renderer', () => {
const helper = read('inc/helpers/aksrt-icon-cards.php');
const helper = read('inc/helpers/jio-icon-cards.php');
// One resolver owns the icon-value contract for both card shapes.
assert.match(helper, /function aksrt_card_icon_inner\(string \$value, string \$extra = ''\): string/);
assert.match(helper, /if \(function_exists\('aksrt_icon_markup'\)\) \{\s*return aksrt_icon_markup\(\$value, \$extra\);/s);
assert.match(helper, /function jio_card_icon_inner\(string \$value, string \$extra = ''\): string/);
assert.match(helper, /if \(function_exists\('jio_icon_markup'\)\) \{\s*return jio_icon_markup\(\$value, \$extra\);/s);
// Both renderers must delegate: emitting a bare FA class string produced
// `<i class="fa fas fa-zibsvg-hot">`, i.e. an invisible glyph, for the two
// SVG formats the admin picker can store.
assert.match(helper, /\$icon_inner = aksrt_card_icon_inner\(\$icon_fa, 'em09'\);/);
assert.match(helper, /\$icon_inner = aksrt_card_icon_inner\(\$icon_fa\);/);
assert.match(helper, /\$icon_inner = jio_card_icon_inner\(\$icon_fa, 'em09'\);/);
assert.match(helper, /\$icon_inner = jio_card_icon_inner\(\$icon_fa\);/);
// The picker prefixes must drive the SVG branches, and the stale
// `aksrtsvg-` prefix (never emitted by any picker) must be gone.
// `jiosvg-` prefix (never emitted by any picker) must be gone.
assert.match(helper, /str_starts_with\(\$value, 'zibsvg-'\) \|\| str_starts_with\(\$value, 'customsvg-'\)/);
assert.doesNotMatch(helper, /aksrtsvg-/);
assert.doesNotMatch(helper, /jiosvg-/);
assert.doesNotMatch(helper, /'fa fas fa-'\s*\.\s*\$icon_fa/);
assert.doesNotMatch(helper, /<i class="'\s*\.\s*esc_attr\(\$icon_fa\)\s*\.\s*' em09"/);
});
@@ -104,14 +104,14 @@ test('custom SVG payloads survive the admin save path for nested icon sub-fields
// header_slider_card_option.cards[].icon is a group sub-field: plain
// sanitize_text_field stripped "customsvg-<svg>" down to a bare prefix.
assert.match(provider, /if \(\$type === 'icon'\) \{\s*return function_exists\('aksrt_sanitize_icon_value'\)/s);
assert.match(metaBox, /'icon'\s*=>\s*function_exists\('aksrt_sanitize_icon_value'\)/);
assert.match(provider, /if \(\$type === 'icon'\) \{\s*return function_exists\('jio_sanitize_icon_value'\)/s);
assert.match(metaBox, /'icon'\s*=>\s*function_exists\('jio_sanitize_icon_value'\)/);
assert.doesNotMatch(metaBox, /'icon'\s*=>\s*sanitize_text_field\(\(string\) \(\$card\['icon'\]/);
});
test('inline custom SVG shapes survive the kses allow list', () => {
const icons = read('inc/helpers/aksrt-icons.php');
const start = icons.indexOf('function aksrt_svg_kses_tags');
const icons = read('inc/helpers/jio-icons.php');
const start = icons.indexOf('function jio_svg_kses_tags');
const ksesBody = icons.slice(start, icons.indexOf('endif;', start));
// Any element the picker's cleanSvg() lets through must also be accepted
@@ -126,7 +126,7 @@ test('inline custom SVG shapes survive the kses allow list', () => {
}
for (const tag of ['circle', 'ellipse', 'rect', 'polygon', 'line', 'path', 'g', 'use']) {
assert.match(ksesBody, new RegExp(`'${tag}'\\s*=>`), `aksrt_svg_kses_tags() is missing <${tag}>`);
assert.match(ksesBody, new RegExp(`'${tag}'\\s*=>`), `jio_svg_kses_tags() is missing <${tag}>`);
}
assert.match(ksesBody, /'viewbox'/);
assert.match(ksesBody, /'xlink:href'/);
@@ -22,27 +22,27 @@ test('universal icon picker ships catalog data for the bundled FA5 fonts', () =>
test('icon catalog REST route is registered with permission + sprite single source', () => {
const admin = read('src/Admin/Admin.php');
assert.match(admin, /\AKSRT\\Icons\\IconController::class/);
assert.match(admin, /\Jio\\Icons\\IconController::class/);
const controller = read('src/Icons/IconController.php');
assert.match(controller, /\/icons\/catalog/);
assert.match(controller, /permission_callback.*check_permission/s);
const catalog = read('src/Icons/IconCatalog.php');
assert.match(catalog, /svg-icon\.js/);
assert.match(catalog, /aksrt_svgs/);
assert.match(catalog, /jio_svgs/);
assert.match(catalog, /fa5-icons\.php/);
});
test('widget and nav-menu icon fields render the shared picker', () => {
const formRenderer = read('src/WidgetSystem/FormRenderer.php');
assert.match(formRenderer, /case 'icon':\s*\n\s*echo aksrt_icon_picker_field\(\$field_name, \$field_id, \(string\) \$value\);/);
assert.doesNotMatch(formRenderer, /aksrt-widget-icon/);
assert.match(formRenderer, /case 'icon':\s*\n\s*echo jio_icon_picker_field\(\$field_name, \$field_id, \(string\) \$value\);/);
assert.doesNotMatch(formRenderer, /jio-widget-icon/);
const adminForm = read('src/WidgetSystem/AdminFormController.php');
assert.match(adminForm, /aksrt_icon_picker_assets\(/);
assert.match(adminForm, /jio_icon_picker_assets\(/);
const navMenu = read('src/Admin/NavMenuOptions.php');
assert.match(navMenu, /aksrt_icon_picker_field\(/);
assert.match(navMenu, /aksrt_sanitize_icon_value\(\(string\) \(\$raw\['icon'\] \?\? ''\)\)/);
const helper = read('inc/helpers/aksrt-icon-picker.php');
for (const token of ['function aksrt_icon_picker_assets', 'function aksrt_icon_picker_field', 'id="aksrt-modal-icon"', 'aksrt-icon-picker__value']) {
assert.match(navMenu, /jio_icon_picker_field\(/);
assert.match(navMenu, /jio_sanitize_icon_value\(\(string\) \(\$raw\['icon'\] \?\? ''\)\)/);
const helper = read('inc/helpers/jio-icon-picker.php');
for (const token of ['function jio_icon_picker_assets', 'function jio_icon_picker_field', 'id="jio-modal-icon"', 'jio-icon-picker__value']) {
assert.match(helper, new RegExp(token));
}
assert.match(helper, /assets\/js\/runtime\/svg-icon\.js/, 'admin previews reuse the frontend sprite');
@@ -50,7 +50,7 @@ test('widget and nav-menu icon fields render the shared picker', () => {
test('picker JS composes the three value formats and loads the REST catalog', () => {
const js = read('assets/js/admin-icon-picker.js');
for (const token of ['X-WP-Nonce', 'customsvg-', 'zibsvg-', '`fa ${group.id} fa-${name}`', 'data-aksrt-icon-picker', 'aksrt-icon-picker__value']) {
for (const token of ['X-WP-Nonce', 'customsvg-', 'zibsvg-', '`fa ${group.id} fa-${name}`', 'data-jio-icon-picker', 'jio-icon-picker__value']) {
assert.match(js, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.match(js, /DOMParser/, 'custom SVG is sanitized client side');
@@ -59,39 +59,39 @@ test('picker JS composes the three value formats and loads the REST catalog', ()
// Tab/search switching toggles the [hidden] attribute; without this guard
// the explicit display rules on grids/items override it and every group
// stays visible, so the tab bar appears dead.
assert.match(read(cssPath), /#aksrt-modal-icon \[hidden]\s*\{\s*display:\s*none\s*!important/);
assert.match(read(cssPath), /#jio-modal-icon \[hidden]\s*\{\s*display:\s*none\s*!important/);
});
test('icon values sanitize through the SVG-aware helper on every save path', () => {
const iconsHelper = read('inc/helpers/aksrt-icons.php');
assert.match(iconsHelper, /function aksrt_sanitize_icon_value/);
assert.match(iconsHelper, /function aksrt_icon_markup/);
assert.match(iconsHelper, /function aksrt_enqueue_fontawesome/);
const iconsHelper = read('inc/helpers/jio-icons.php');
assert.match(iconsHelper, /function jio_sanitize_icon_value/);
assert.match(iconsHelper, /function jio_icon_markup/);
assert.match(iconsHelper, /function jio_enqueue_fontawesome/);
assert.match(iconsHelper, /str_starts_with\(\$value, 'customsvg-'\)/);
const schemaProvider = read('src/Admin/SchemaProvider.php');
assert.match(schemaProvider, /'icon'\s+=>\s+'icon'/);
assert.match(schemaProvider, /case 'icon':\s*\n\s*return function_exists\('aksrt_sanitize_icon_value'\)/);
assert.match(schemaProvider, /case 'icon':\s*\n\s*return function_exists\('jio_sanitize_icon_value'\)/);
const fieldSchema = read('src/WidgetSystem/FieldSchema.php');
assert.match(fieldSchema, /case 'icon':\s*\n\s*return function_exists\('aksrt_sanitize_icon_value'\)/);
assert.match(fieldSchema, /case 'icon':\s*\n\s*return function_exists\('jio_sanitize_icon_value'\)/);
});
test('FA5 loads under a single canonical handle everywhere', () => {
const picker = read('inc/helpers/aksrt-icon-picker.php');
const picker = read('inc/helpers/jio-icon-picker.php');
const admin = read('src/Admin/Admin.php');
// 文章列表(edit.php)的 FA 入队随列增强迁到了 PostListEnhance。
const postList = read('src/Admin/Native/PostListEnhance.php');
const part1b = read('src/Core/ScriptsPart1b.php');
assert.match(picker, /aksrt_enqueue_fontawesome\(\)/);
assert.match(admin, /aksrt_enqueue_fontawesome\(\)/);
assert.match(postList, /aksrt_enqueue_fontawesome\(\)/);
assert.match(part1b, /aksrt_enqueue_fontawesome\(\['bootstrap'\]\)/);
assert.match(part1b, /\['aksrt-font-awesome'\]/, 'public main css depends on the canonical handle');
assert.match(picker, /jio_enqueue_fontawesome\(\)/);
assert.match(admin, /jio_enqueue_fontawesome\(\)/);
assert.match(postList, /jio_enqueue_fontawesome\(\)/);
assert.match(part1b, /jio_enqueue_fontawesome\(\['bootstrap'\]\)/);
assert.match(part1b, /\['jio-font-awesome'\]/, 'public main css depends on the canonical handle');
for (const file of [picker, admin, postList, part1b]) {
assert.doesNotMatch(file, /wp_enqueue_style\(\s*'aksrt-font-awesome'/, 'no duplicate FA registrations');
assert.doesNotMatch(file, /wp_enqueue_style\(\s*'jio-font-awesome'/, 'no duplicate FA registrations');
}
// core-hooks.php 不该再自己注册或重复入队 FA。
assert.doesNotMatch(read('inc/core-hooks.php'), /aksrt_enqueue_fontawesome|wp_enqueue_style\(\s*'aksrt-font-awesome'/);
for (const file of ['inc/helpers/aksrt-widgets-markup.php', 'src/WidgetSystem/Renderers/ContentRenderer.php']) {
assert.doesNotMatch(read('inc/core-hooks.php'), /jio_enqueue_fontawesome|wp_enqueue_style\(\s*'jio-font-awesome'/);
for (const file of ['inc/helpers/jio-widgets-markup.php', 'src/WidgetSystem/Renderers/ContentRenderer.php']) {
const source = read(file);
assert.doesNotMatch(source, /fa-clock-o|fa-folder-open-o|fa-file-text-o/, `${file} uses FA5 names`);
}
@@ -119,13 +119,13 @@ test('custom settings pages and standalone save paths speak the three icon forma
assert.match(read('assets/admin/src/components/floating/FloatMoreButtonsEditor.vue'), /import IconValue from '@\/components\/form\/IconValue\.vue'/);
// Their REST save paths keep customsvg markup intact.
const controller = read('src/Admin/GlobalControllerPart1.php');
assert.match(controller, /aksrt_sanitize_icon_value\(/);
assert.match(controller, /jio_sanitize_icon_value\(/);
assert.doesNotMatch(controller, /preg_match\('\/\^fa\\s\+/, 'no FA-only regex gate on icon fields');
assert.match(read('src/Settings/FloatButtons.php'), /aksrt_sanitize_icon_value\(/);
assert.match(read('src/Settings/FloatButtons.php'), /jio_sanitize_icon_value\(/);
// Frontend consumers render every format.
const footer = read('footer.php');
assert.match(footer, /aksrt_icon_markup\(\(string\) \$tab_icon\)/);
assert.match(read('inc/helpers/aksrt-float-buttons.php'), /aksrt_icon_markup\(\(string\) \(\$opt\['icon'\]/);
assert.match(footer, /jio_icon_markup\(\(string\) \$tab_icon\)/);
assert.match(read('inc/helpers/jio-float-buttons.php'), /jio_icon_markup\(\(string\) \(\$opt\['icon'\]/);
// The popover tabs must wrap instead of overflowing the panel.
assert.match(read('assets/admin/src/components/form/FaIconPicker.vue'), /flex-wrap: wrap/);
});
@@ -133,12 +133,12 @@ test('custom settings pages and standalone save paths speak the three icon forma
test('nav menu icons render every format without legacy fa-prefix mangling', () => {
const helper = read('inc/helpers/menu-decoration.php');
assert.match(helper, /str_starts_with\(\$icon_class, 'zibsvg-'\)/);
assert.match(helper, /aksrt_icon_markup\(\$icon_class, 'icon mr3 menu-item-icon'\)/);
assert.match(helper, /jio_icon_markup\(\$icon_class, 'icon mr3 menu-item-icon'\)/);
assert.doesNotMatch(helper, /'<i class="' \. esc_attr\(\$icon_class\)/, 'no raw i-tag bypassing the unified helper');
});
test('icon markup upgrades legacy FA4 values to FA5 and never emits the FA4 form', () => {
const helper = read('inc/helpers/aksrt-icons.php');
const helper = read('inc/helpers/jio-icons.php');
// Bare glyph, legacy double-fa value and style-only value all normalize to FA5 solid.
assert.match(helper, /preg_match\('\/\^fa\\s\+fa-\/i', \$value\)/, 'legacy double-fa value is detected');
assert.match(helper, /preg_replace\('\/\^fa\\s\+\/i', 'fa fas ', \$value, 1\)/, 'legacy double-fa value upgrades to FA5');
@@ -156,7 +156,7 @@ test('brand icons render despite the zibll-legacy fa-fab double prefix', () => {
}
assert.match(read('assets/css/main.min.css'), rule, 'minified main css carries the fix');
// Empty sprite entries surface as blank picker items — none may ship.
assert.doesNotMatch(read('assets/js/runtime/svg-icon.js'), /aksrt_svgs\['[a-zA-Z0-9_]+'\]\s*=\s*'';/);
assert.doesNotMatch(read('assets/js/runtime/svg-icon.js'), /jio_svgs\['[a-zA-Z0-9_]+'\]\s*=\s*'';/);
});
test('icon assets are consolidated and dead icon assets are gone', () => {
@@ -28,7 +28,7 @@ function input(html, name) {
return match[0];
}
function fieldset(html, dependency) {
const tags = [...html.matchAll(/<fieldset\b[^>]*data-aksrt-inv-dep="([^"]+)"[^>]*>/g)];
const tags = [...html.matchAll(/<fieldset\b[^>]*data-jio-inv-dep="([^"]+)"[^>]*>/g)];
const found = tags.find(([, value]) => value === dependency);
assert.ok(found, `missing conditional fieldset ${dependency}`);
return found[0];
@@ -42,18 +42,18 @@ function hasBooleanAttr(tag, name) {
test('filters, selection, table and pagination share one list card', () => {
const { html } = harness();
const cards = [...html.matchAll(/<section\b[^>]*class="[^"]*\baksrt-native-card\b[^"]*"[^>]*>[\s\S]*?<\/section>/g)];
const cards = [...html.matchAll(/<section\b[^>]*class="[^"]*\bjio-native-card\b[^"]*"[^>]*>[\s\S]*?<\/section>/g)];
assert.equal(cards.length, 1);
const card = cards[0][0];
assert.match(card, /aksrt-inv-list-card/);
assert.match(card, /<h2 class="aksrt-native-card__title">邀请码列表<\/h2>/);
for (const marker of ['class="subsubsub"', 'class="aksrt-native-filters"', '清除筛选', 'data-aksrt-inv-selection', 'aksrt-inv-table', 'class="tablenav bottom"']) {
assert.match(card, /jio-inv-list-card/);
assert.match(card, /<h2 class="jio-native-card__title">邀请码列表<\/h2>/);
for (const marker of ['class="subsubsub"', 'class="jio-native-filters"', '清除筛选', 'data-jio-inv-selection', 'jio-inv-table', 'class="tablenav bottom"']) {
assert.ok(card.includes(marker), `${marker} belongs to the list card`);
}
// 批量操作行同时带共享的 aksrt-native-bulk 类,按 class 列表中的词匹配,不要求整串相等。
assert.match(card, /class="[^"]*\baksrt-inv-bulk\b[^"]*"/, 'aksrt-inv-bulk belongs to the list card');
assert.doesNotMatch(html, /aksrt-inv-filter-summary|关键词:/);
assert.doesNotMatch(read('assets', 'css', 'admin', 'invite-codes.css'), /aksrt-inv-filter-summary/);
// 批量操作行同时带共享的 jio-native-bulk 类,按 class 列表中的词匹配,不要求整串相等。
assert.match(card, /class="[^"]*\bjio-inv-bulk\b[^"]*"/, 'jio-inv-bulk belongs to the list card');
assert.doesNotMatch(html, /jio-inv-filter-summary|关键词:/);
assert.doesNotMatch(read('assets', 'css', 'admin', 'invite-codes.css'), /jio-inv-filter-summary/);
});
test('search and bulk actions remain independent sibling forms inside the list card', () => {
@@ -71,8 +71,8 @@ test('search and bulk actions remain independent sibling forms inside the list c
}
assert.equal(depth, 0);
assert.equal(count, 4);
const searchEnd = html.indexOf('</form>', html.indexOf('class="aksrt-native-filters"'));
const listStart = html.lastIndexOf('<form', html.indexOf('data-aksrt-inv-list'));
const searchEnd = html.indexOf('</form>', html.indexOf('class="jio-native-filters"'));
const listStart = html.lastIndexOf('<form', html.indexOf('data-jio-inv-list'));
assert.ok(searchEnd < listStart);
const listEnd = html.indexOf('</form>', listStart);
assert.ok(html.indexOf('class="tablenav bottom"', listStart) < listEnd);
@@ -81,13 +81,13 @@ test('search and bulk actions remain independent sibling forms inside the list c
test('page renders add and export dialogs beside, not inside, the list form', () => {
const { html } = harness();
const listMarker = html.indexOf('data-aksrt-inv-list');
const listMarker = html.indexOf('data-jio-inv-list');
assert.ok(listMarker > 0, 'list form is rendered');
const listStart = html.lastIndexOf('<form', listMarker);
const listEnd = html.indexOf('</form>', listMarker);
const addStart = html.indexOf('<dialog', html.indexOf('id="aksrt-inv-add"') - 100);
const addDialog = dialog(html, 'aksrt-inv-add');
const exportDialog = dialog(html, 'aksrt-inv-export');
const addStart = html.indexOf('<dialog', html.indexOf('id="jio-inv-add"') - 100);
const addDialog = dialog(html, 'jio-inv-add');
const exportDialog = dialog(html, 'jio-inv-export');
const addIndex = html.indexOf(addDialog);
const exportIndex = html.indexOf(exportDialog);
assert.ok(listStart >= 0 && listEnd > listStart);
@@ -100,18 +100,18 @@ test('page renders add and export dialogs beside, not inside, the list form', ()
test('all admin forms carry the expected method, action, and escaped nonce', () => {
const { html } = harness();
const list = html.slice(html.lastIndexOf('<form', html.indexOf('data-aksrt-inv-list')), html.indexOf('</form>', html.indexOf('data-aksrt-inv-list')) + 7);
const add = dialog(html, 'aksrt-inv-add');
const exportForm = dialog(html, 'aksrt-inv-export');
const list = html.slice(html.lastIndexOf('<form', html.indexOf('data-jio-inv-list')), html.indexOf('</form>', html.indexOf('data-jio-inv-list')) + 7);
const add = dialog(html, 'jio-inv-add');
const exportForm = dialog(html, 'jio-inv-export');
assert.match(list, /<form method="post" action="https:\/\/example\.test\/wp-admin\/admin-post\.php"/);
assert.match(list, /name="action" value="aksrt_invite_delete"/);
assert.match(list, /name="action" value="jio_invite_delete"/);
for (const form of [list, add, exportForm]) {
assert.match(form, /name="aksrt_nonce" value="nonce&quot;&lt;script&gt;&amp;"/);
assert.match(form, /name="jio_nonce" value="nonce&quot;&lt;script&gt;&amp;"/);
}
assert.match(add, /<form method="post" action="https:\/\/example\.test\/wp-admin\/admin-post\.php"/);
assert.match(add, /name="action" value="aksrt_invite_add"/);
assert.match(add, /name="action" value="jio_invite_add"/);
assert.match(exportForm, /<form method="get" action="https:\/\/example\.test\/wp-admin\/admin-post\.php"/);
assert.match(exportForm, /name="action" value="aksrt_invite_export"/);
assert.match(exportForm, /name="action" value="jio_invite_export"/);
});
test('rendered code, notice, user, date, and row values are escaped', () => {
@@ -162,20 +162,20 @@ test('numeric fields expose browser constraints and VIP days apply to both level
test('export UI and service consistently name the spreadsheet format CSV', () => {
const { html } = harness();
const exportForm = dialog(html, 'aksrt-inv-export');
const exportForm = dialog(html, 'jio-inv-export');
assert.match(exportForm, /name="export_format" value="csv" checked/);
assert.match(exportForm, /Excel 表格(\.csv)/);
assert.match(exportForm, /文本文档(\.txt)/);
const service = read('src', 'Payment', 'InviteCode.php');
assert.match(service, /\$_GET\['export_format'\] \?\? 'csv'\) === 'text' \? 'text' : 'csv'/);
assert.match(service, /\[__\('邀请码', 'aksrt'\), __\('使用奖励', 'aksrt'\), __\('创建时间', 'aksrt'\), __\('更新时间', 'aksrt'\), __\('状态', 'aksrt'\), __\('标识', 'aksrt'\)\]/);
assert.match(service, /aksrt-invite-codes\.csv/);
assert.match(service, /\[__\('邀请码', 'jio'\), __\('使用奖励', 'jio'\), __\('创建时间', 'jio'\), __\('更新时间', 'jio'\), __\('状态', 'jio'\), __\('标识', 'jio'\)\]/);
assert.match(service, /jio-invite-codes\.csv/);
assert.doesNotMatch(service, /\bxls\b|export_format[^\n]*xls/);
});
test('search form and filter links preserve sanitized keyword and remark context', () => {
const { html } = harness();
const marker = html.indexOf('class="aksrt-native-filters"');
const marker = html.indexOf('class="jio-native-filters"');
const start = html.lastIndexOf('<form', marker);
const end = html.indexOf('</form>', marker);
const form = html.slice(start, end + 7);
@@ -210,7 +210,7 @@ test('page uses no list/add/export tab route and service returns to the canonica
const page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php');
const forms = read('src', 'Admin', 'Native', 'UsersMenusInviteForms.php');
assert.doesNotMatch(page + forms + html, /page-title-action|[?&](?:tab|view)=(?:list|add|export)|tab=(?:list|add|export)/);
assert.equal(return_url, 'https://example.test/wp-admin/users.php?page=aksrt_invite_codes');
assert.equal(return_url, 'https://example.test/wp-admin/users.php?page=jio_invite_codes');
assert.doesNotMatch(return_url, /(?:tab|view|add)=/);
});
@@ -229,9 +229,9 @@ test('invite CSS and JS are enqueued only for the invite submenu hook', () => {
assert.deepEqual(wrong_hook_assets, []);
assert.equal(page_assets.length, 2);
assert.deepEqual(page_assets[0].slice(0, 4), [
'style', 'aksrt-admin-invite-codes', 'https://example.test/theme/assets/css/admin/invite-codes.css', ['aksrt-admin-native-ui'],
'style', 'jio-admin-invite-codes', 'https://example.test/theme/assets/css/admin/invite-codes.css', ['jio-admin-native-ui'],
]);
assert.deepEqual(page_assets[1].slice(0, 4), [
'script', 'aksrt-admin-invite-codes', 'https://example.test/theme/assets/js/admin-invite-codes.js', [],
'script', 'jio-admin-invite-codes', 'https://example.test/theme/assets/js/admin-invite-codes.js', [],
]);
});
+13 -13
View File
@@ -1,16 +1,16 @@
<?php
/** WordPress-free render harness for the native invite admin page. */
namespace AKSRT\Core {
namespace Jio\Core {
final class DBMigrator {
public static function table($key) { return 'wp_aksrt_' . $key; }
public static function table($key) { return 'wp_jio_' . $key; }
}
}
namespace {
define('ABSPATH', __DIR__);
define('ARRAY_A', 'ARRAY_A');
$root = dirname(__DIR__, 2);
define('AKSRT_DIR', $root);
define('AKSRT_URI', 'https://example.test/theme');
define('JIO_DIR', $root);
define('JIO_URI', 'https://example.test/theme');
class WP_Error {
public $code;
@@ -88,10 +88,10 @@ namespace {
return ['all_count' => 2, 'unused_count' => 1, 'used_count' => 1];
}
public function get_results($sql, $format = null) {
if (str_contains($sql, 'FROM wp_aksrt_invite_code_usage u')) {
if (str_contains($sql, 'FROM wp_jio_invite_code_usage u')) {
return [['code_id' => 41, 'user_id' => 7, 'used_at' => '2026-10-30 11:00:00', 'display_name' => 'Admin"><script>alert(1)</script>']];
}
if (str_contains($sql, 'SELECT * FROM wp_aksrt_invite_code')) {
if (str_contains($sql, 'SELECT * FROM wp_jio_invite_code')) {
return [
[
'id' => 41,
@@ -133,18 +133,18 @@ namespace {
require $root . '/src/Admin/Native/UsersMenusInviteCode.php';
final class InviteAdminProbe {
use \AKSRT\Admin\Native\UsersMenusInviteCode;
use \Jio\Admin\Native\UsersMenusInviteCode;
}
$_GET = [
'page' => 'aksrt_invite_codes',
'page' => 'jio_invite_codes',
'status' => 'used',
's' => 'search <script>alert(2)</script> & phrase',
'remark' => 'campaign & east',
'orderby' => 'code',
'desc' => 'ASC',
'aksrt_inv_title' => '<script>alert(1)</script>Completed',
'aksrt_inv_msg' => '<img src=x onerror=alert(1)>notice',
'jio_inv_title' => '<script>alert(1)</script>Completed',
'jio_inv_msg' => '<img src=x onerror=alert(1)>notice',
];
$probe = new InviteAdminProbe();
ob_start();
@@ -152,12 +152,12 @@ namespace {
$page = ob_get_clean();
$GLOBALS['assets'] = [];
$probe->enqueue_invite_assets('users_page_aksrt_messages');
$probe->enqueue_invite_assets('users_page_jio_messages');
$wrong_hook_assets = $GLOBALS['assets'];
$probe->enqueue_invite_assets('users_page_aksrt_invite_codes');
$probe->enqueue_invite_assets('users_page_jio_invite_codes');
$page_assets = $GLOBALS['assets'];
$service = \AKSRT\Payment\InviteCode::get_instance();
$service = \Jio\Payment\InviteCode::get_instance();
$guard = new \ReflectionMethod($service, 'guard_and_redirect');
$list_result = $service->list_codes(['status' => 'used']);
$import_lines = [];
@@ -8,13 +8,13 @@ const read = (...segments) => fs.readFileSync(path.join(root, ...segments), 'utf
test('invite code table has a unique code key and a usage log table', () => {
const schema = read('src', 'Core', 'DBMigratorPart2.php')
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_invite_code \([\s\S]*?UNIQUE KEY uniq_invite_code \(code\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_invite_code \([\s\S]*?\) ENGINE=InnoDB/)
assert.match(schema, /CREATE TABLE \{\$prefix\}jio_invite_code \([\s\S]*?UNIQUE KEY uniq_invite_code \(code\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}jio_invite_code \([\s\S]*?\) ENGINE=InnoDB/)
// 使用记录表:运营需要知道"哪个用户用了哪个码",且 (code_id, user_id) 唯一防重复记账。
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_invite_code_usage \([\s\S]*?UNIQUE KEY uniq_invite_usage \(code_id, user_id\)/)
assert.match(schema, /CREATE TABLE \{\$prefix\}jio_invite_code_usage \([\s\S]*?UNIQUE KEY uniq_invite_usage \(code_id, user_id\)/)
const registry = read('src', 'Core', 'DBMigratorPart3.php')
assert.match(registry, /'invite_code'\s+=>\s+'aksrt_invite_code'/)
assert.match(registry, /'invite_code_usage'\s+=>\s+'aksrt_invite_code_usage'/)
assert.match(registry, /'invite_code'\s+=>\s+'jio_invite_code'/)
assert.match(registry, /'invite_code_usage'\s+=>\s+'jio_invite_code_usage'/)
})
test('consumption is a single conditional UPDATE, not select-then-update', () => {
@@ -50,7 +50,7 @@ test('registration services gate on the invite-code field mode', () => {
test('rewards defer to email verification when signup verification is on', () => {
const service = read('src', 'Payment', 'InviteCode.php')
assert.match(service, /signup_verification_enabled\(\)/)
assert.match(service, /aksrt_email_verified/)
assert.match(service, /jio_email_verified/)
assert.match(service, /META_PENDING/)
})
@@ -71,9 +71,9 @@ test('invite-code surfaces: form field, admin page, menus and both schemas', ()
assert.match(page, /orderby-but/)
assert.match(page, /bulk_action/)
assert.match(page, /paginate_links/)
assert.match(page, /aksrt_invite_delete/)
assert.match(forms, /aksrt_invite_add/)
assert.match(forms, /aksrt_invite_export/)
assert.match(page, /jio_invite_delete/)
assert.match(forms, /jio_invite_add/)
assert.match(forms, /jio_invite_export/)
// 列表内不再有列表/添加/导出三视图路由;状态筛选仍是独立的列表筛选器。
assert.doesNotMatch(page + forms, /page-title-action|[?&](?:tab|view)=(?:add|export)|tab=(?:list|add|export)/)
// 管理页不再依赖 REST 与 wp_rest nonce。
@@ -81,7 +81,7 @@ test('invite-code surfaces: form field, admin page, menus and both schemas', ()
const menus = read('src', 'Admin', 'Native', 'UsersMenus.php')
assert.match(menus, /use UsersMenusInviteCode/)
assert.match(menus, /aksrt_invite_codes/)
assert.match(menus, /jio_invite_codes/)
const bootstrap = read('functions.php')
assert.match(bootstrap, /InviteCode::get_instance\(\)->init\(\)/)
@@ -97,14 +97,14 @@ test('invite-code surfaces: form field, admin page, menus and both schemas', ()
test('admin operations run through admin_post handlers with capability and nonce guards', () => {
const service = read('src', 'Payment', 'InviteCode.php')
assert.match(service, /admin_post_aksrt_invite_add/)
assert.match(service, /admin_post_aksrt_invite_delete/)
assert.match(service, /admin_post_aksrt_invite_export/)
assert.match(service, /check_admin_referer\('aksrt_invite_admin', 'aksrt_nonce'\)/)
assert.match(service, /admin_post_jio_invite_add/)
assert.match(service, /admin_post_jio_invite_delete/)
assert.match(service, /admin_post_jio_invite_export/)
assert.match(service, /check_admin_referer\('jio_invite_admin', 'jio_nonce'\)/)
assert.match(service, /current_user_can\('edit_users'\)/)
// 管理端 REST 已移除:除公开 verify 外不再注册邀请码路由(导出文件名除外)。
assert.doesNotMatch(service, /register_rest_route\('aksrt\/v1', '\/invite-codes/)
assert.match(service, /register_rest_route\('aksrt\/v1', '\/invite-code\/verify'/)
assert.doesNotMatch(service, /register_rest_route\('jio\/v1', '\/invite-codes/)
assert.match(service, /register_rest_route\('jio\/v1', '\/invite-code\/verify'/)
// 列表/导出共用状态过滤;批量删除连使用记录一起清。
assert.match(service, /private function status_where\(string \$status\): array/)
assert.match(service, /'unused' => \['used_count = 0', \[\]\]/)
@@ -157,8 +157,8 @@ test('per-code remark persists through generate/import and both exports round-tr
assert.match(service, /\$parts === \[\] \? '无'/)
// 文本导出与导入格式互通;CSV(Excel)含中文表头与状态列。
assert.match(service, /\$row\['code'\] \. \$division \. \$this->reward_param_string/)
assert.match(service, /__\('邀请码', 'aksrt'\), __\('使用奖励', 'aksrt'\)/)
assert.match(service, /\$used > 0 \? __\('已使用', 'aksrt'\) : __\('未使用', 'aksrt'\)/)
assert.match(service, /__\('邀请码', 'jio'\), __\('使用奖励', 'jio'\)/)
assert.match(service, /\$used > 0 \? __\('已使用', 'jio'\) : __\('未使用', 'jio'\)/)
const forms = read('src', 'Admin', 'Native', 'UsersMenusInviteForms.php')
// 自动生成标识字段默认 invita_YmdHis;列表标识列可点击定位。
+22 -22
View File
@@ -53,9 +53,9 @@ function sanitize_key($value) { return preg_replace('/[^a-z0-9_\\-]/', '', strto
function esc_attr($value) { return htmlspecialchars((string) $value, ENT_QUOTES); }
function esc_url($value) { return (string) $value; }
function get_template_directory_uri() { return 'https://theme.test'; }
function aksrt_get_default_avatar_url() { return 'https://theme.test/default-avatar.png'; }
eval('namespace AKSRT\\\\Settings; class Settings { public static $values = []; public static function get_value($key, $default = null) { return array_key_exists($key, self::$values) ? self::$values[$key] : $default; } }');
\\AKSRT\\Settings\\Settings::$values = json_decode(${JSON.stringify(JSON.stringify(values))}, true);
function jio_get_default_avatar_url() { return 'https://theme.test/default-avatar.png'; }
eval('namespace Jio\\\\Settings; class Settings { public static $values = []; public static function get_value($key, $default = null) { return array_key_exists($key, self::$values) ? self::$values[$key] : $default; } }');
\\Jio\\Settings\\Settings::$values = json_decode(${JSON.stringify(JSON.stringify(values))}, true);
require ${JSON.stringify(helper)};
${body}
`;
@@ -89,32 +89,32 @@ test('admin schemas expose the ten legacy lazy fields and defaults', () => {
});
test('PHP helpers emit placeholder/data-src, direct src, and crawler bypass', () => {
const enabled = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');");
const enabled = phpHarness("echo jio_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');");
assert.match(enabled, /class="lazyload fit-cover"/);
assert.match(enabled, /src="https:\/\/theme\.test\/assets\/img\/thumbnail\.svg"/);
assert.match(enabled, /data-src="https:\/\/cdn\.test\/post\.webp"/);
const disabled = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", { lazy_posts_thumb: false });
const disabled = phpHarness("echo jio_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", { lazy_posts_thumb: false });
assert.equal(disabled, 'class="fit-cover" src="https://cdn.test/post.webp"');
const crawler = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", {}, 'Googlebot/2.1');
const crawler = phpHarness("echo jio_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", {}, 'Googlebot/2.1');
assert.equal(crawler, 'class="fit-cover" src="https://cdn.test/post.webp"');
const empty = phpHarness("echo aksrt_get_lazy_attr('lazy_other', '', 'media');");
const empty = phpHarness("echo jio_get_lazy_attr('lazy_other', '', 'media');");
assert.equal(empty, 'class="media" src=""');
});
test('content, avatar, and responsive image paths follow one contract', () => {
const content = phpHarness(`
$html = '<img class="hero" src="https://cdn.test/photo.jpg" srcset="https://cdn.test/photo-2x.jpg 2x">';
echo aksrt_lazy_content_images($html);
echo jio_lazy_content_images($html);
`, { lazy_posts_content: true });
assert.match(content, /thumbnail-lg\.svg/);
assert.match(content, /data-src="https:\/\/cdn\.test\/photo\.jpg"/);
assert.match(content, /data-srcset="https:\/\/cdn\.test\/photo-2x\.jpg 2x"/);
assert.doesNotMatch(content, /\ssrcset=/);
const avatar = phpHarness("echo aksrt_lazy_avatar_html('<img class=\"avatar\" src=\"https://cdn.test/avatar.png\" loading=\"lazy\">');");
const avatar = phpHarness("echo jio_lazy_avatar_html('<img class=\"avatar\" src=\"https://cdn.test/avatar.png\" loading=\"lazy\">');");
assert.match(avatar, /class="lazyload avatar"/);
assert.match(avatar, /src="https:\/\/theme\.test\/default-avatar\.png"/);
assert.match(avatar, /data-src="https:\/\/cdn\.test\/avatar\.png"/);
@@ -128,7 +128,7 @@ test('lazySizes has one auto_fun integration owner and loader boots main once',
const routeScripts = read('src/Core/ScriptsPart1b.php');
const builder = read('tools/build-public-assets.mjs');
const loader = read('assets/js/runtime/loader.js');
const searchShell = read('inc/helpers/header/aksrt-header-menu-search.php');
const searchShell = read('inc/helpers/header/jio-header-menu-search.php');
const pagination = read('assets/js/core/pagination.js');
const widgets = read('assets/widgets/frontend.js');
@@ -152,7 +152,7 @@ test('lazySizes has one auto_fun integration owner and loader boots main once',
assert.match(runtime, /var lazyload_action = _this\.attr\('lazyload-action'\)/);
assert.match(runtime, /if \(_this\.attr\('remote-box'\)\)[\s\S]*?_this\.click\(\)/);
assert.match(searchShell, /'type'\s*=>\s*'click'/);
assert.match(runtime, /aksrt:remote-loaded/);
assert.match(runtime, /jio:remote-loaded/);
assert.doesNotMatch(runtime, /action:\s*'get_current_user'/);
assert.match(runtime, /var cases = jQuery\('\.vanilla-tilt:not\(\.is-initialized\)'\);[\s\S]*?if \(!cases\.length\)[\s\S]*?tbquire\(\['tilt'\]/);
assert.doesNotMatch(pagination, /IntersectionObserver|new Image\(/);
@@ -164,18 +164,18 @@ test('lazySizes has one auto_fun integration owner and loader boots main once',
read('assets/js/pay-vip.js'),
loader,
].join('\n');
const legacyDynamicImageApi = 'aksrt' + 'GetLazyImageAttr';
const legacyDynamicImageApi = 'jio' + 'GetLazyImageAttr';
assert.equal(dynamicSources.includes(legacyDynamicImageApi), false);
assert.match(read('assets/js/order-center.js'), /<img src="' \+ escapeHtml\(image\)/);
assert.match(read('assets/js/page-navs.js'), /<img src="' \+ this\.escHtml\(item\.thumbnail\)/);
assert.match(read('assets/js/pay-vip.js'), /document\.createElement\("img"\)/);
});
test('widget video init flag stays out of the AksrtVideoPlayer attribute namespace', () => {
test('widget video init flag stays out of the JioVideoPlayer attribute namespace', () => {
const widgets = read('assets/widgets/frontend.js');
// data-aksrt-video 是 video-player.js 的自动初始化选择器:小工具容器若带上
// data-jio-video 是 video-player.js 的自动初始化选择器:小工具容器若带上
// 该属性,会被再挂一个无源播放器(黑条 + 遮罩叠在 DPlayer 上)。
assert.doesNotMatch(widgets, /dataset\.aksrtVideo\s*=/);
assert.doesNotMatch(widgets, /dataset\.jioVideo\s*=/);
assert.match(widgets, /once\(el, 'WidgetVideo'\)/);
});
@@ -190,9 +190,9 @@ test('post lists have no second lazy switch and keep legacy fallback thumbnails'
for (const source of [config, postData, markup, phpSchema, vueSchema]) {
assert.equal(source.includes(legacyKey), false);
}
assert.match(postData, /aksrt_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/);
assert.match(markup, /aksrt_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/);
assert.match(config, /\$primary = aksrt_renderer_spare_image\(\$thumb\);[\s\S]*?\$primary = \(string\) aksrt_get_lazy_thumb\(\);/);
assert.match(postData, /jio_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/);
assert.match(markup, /jio_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/);
assert.match(config, /\$primary = jio_renderer_spare_image\(\$thumb\);[\s\S]*?\$primary = \(string\) jio_get_lazy_thumb\(\);/);
assert.doesNotMatch(config, /rand_thumb/);
});
@@ -203,13 +203,13 @@ test('legacy lazy identifiers and theme-native loading attributes are gone', ()
'data-' + 'lazy-animation',
'lazyload_' + 'enabled',
'get_' + 'lazyload_config',
'aksrt_' + 'lazy_img_attrs',
'aksrt_' + 'lazy_loading',
'jio_' + 'lazy_img_attrs',
'jio_' + 'lazy_loading',
'thumb_' + 'lazy',
];
for (const token of forbidden) assert.equal(source.includes(token), false, token);
assert.doesNotMatch(source, /aksrt-lazy(?!sizes|-image-attr)/);
assert.doesNotMatch(source, /aksrt-loaded/);
assert.doesNotMatch(source, /jio-lazy(?!sizes|-image-attr)/);
assert.doesNotMatch(source, /jio-loaded/);
assert.doesNotMatch(source, /loading=["'](?:lazy|eager)["']/);
assert.doesNotMatch(source, /\.loading\s*=\s*["'](?:lazy|eager)["']/);
assert.doesNotMatch(source, /setAttribute\(\s*["']loading["']/);
@@ -1,7 +1,7 @@
'use strict';
/**
* 消息管理后台(users.php → aksrt_messages)契约:
* 消息管理后台(users.php → jio_messages)契约:
* 1) 页面由独立 trait 承载,UsersMenus 只做组合与路由注册;
* 2) 写操作走 admin_post + nonce + 能力校验;
* 3) SQL 全部在 NotificationAdminQuery 内且经 prepare;
@@ -20,25 +20,25 @@ const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('messages page is composed from its own trait', () => {
const menus = read('src/Admin/Native/UsersMenus.php');
assert.ok(menus.includes('use UsersMenusMessages;'));
assert.ok(menus.includes("'aksrt_messages'"));
assert.ok(menus.includes("add_action('admin_post_aksrt_users_send_message', [$this, 'handle_send_message'])"));
assert.ok(menus.includes("add_action('admin_post_aksrt_users_bulk_messages', [$this, 'handle_bulk_messages'])"));
assert.ok(menus.includes("'jio_messages'"));
assert.ok(menus.includes("add_action('admin_post_jio_users_send_message', [$this, 'handle_send_message'])"));
assert.ok(menus.includes("add_action('admin_post_jio_users_bulk_messages', [$this, 'handle_bulk_messages'])"));
assert.ok(menus.includes("add_action('admin_enqueue_scripts', [$this, 'enqueue_message_assets'])"));
// 旧实现不得残留在壳类中。
assert.doesNotMatch(menus, /function render_messages_page|function handle_send_message|message_sent|message_failed/);
assert.doesNotMatch(menus, /aksrt_notification/);
assert.doesNotMatch(menus, /jio_notification/);
});
test('write handlers check capability and nonce', () => {
const page = read('src/Admin/Native/UsersMenusMessages.php');
assert.match(page, /function handle_send_message\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('aksrt_users_send_message'\);/);
assert.match(page, /function handle_bulk_messages\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('aksrt_users_bulk_messages'\);/);
assert.match(page, /function handle_send_message\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('jio_users_send_message'\);/);
assert.match(page, /function handle_bulk_messages\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('jio_users_bulk_messages'\);/);
const compose = read('src/Admin/Native/UsersMenusMessagesCompose.php');
const list = read('src/Admin/Native/UsersMenusMessagesList.php');
assert.ok(page.includes('use UsersMenusMessagesCompose;'));
assert.ok(page.includes('use UsersMenusMessagesList;'));
assert.ok(compose.includes("wp_nonce_field('aksrt_users_send_message')"));
assert.ok(list.includes("wp_nonce_field('aksrt_users_bulk_messages')"));
assert.ok(compose.includes("wp_nonce_field('jio_users_send_message')"));
assert.ok(list.includes("wp_nonce_field('jio_users_bulk_messages')"));
// 页面层不直接碰数据库,分页走共享组件。
assert.doesNotMatch(page + compose + list, /\$wpdb/);
assert.ok(page.includes('AdminListPagination::render('));
@@ -53,7 +53,7 @@ test('admin query keeps SQL prepared and bulk size capped', () => {
});
test('dead notification helpers are removed', () => {
assert.doesNotMatch(read('inc/modules/user-notifications.php'), /aksrt_notification_type_label/);
assert.doesNotMatch(read('inc/modules/user-notifications.php'), /jio_notification_type_label/);
assert.doesNotMatch(read('src/Messages/Message.php'), /get_all_notifications/);
assert.ok(fs.existsSync(path.join(root, 'assets/css/admin/messages.css')));
});
@@ -72,10 +72,10 @@ test('list toolbar is merged into the table card and inline confirmation is remo
const page = read('src/Admin/Native/UsersMenusMessages.php');
const list = read('src/Admin/Native/UsersMenusMessagesList.php');
assert.ok(page.indexOf("NativeUi::card_open(__('站内通知'") < page.indexOf('$this->render_message_filter_form'));
assert.doesNotMatch(page + list, /onclick=|confirm\(|<details|aksrt-native-toolbar/);
assert.doesNotMatch(page + list, /onclick=|confirm\(|<details|jio-native-toolbar/);
const { table, empty } = harness();
assert.match(table, /data-aksrt-msg-action="delete" disabled/);
assert.match(table, /data-aksrt-msg-selection/);
assert.match(table, /data-jio-msg-action="delete" disabled/);
assert.match(table, /data-jio-msg-selection/);
assert.match(table, /name="period" value="today"/);
assert.match(empty, /当前筛选条件下没有消息/);
});
@@ -89,27 +89,27 @@ test('message row actions live only in a dedicated rightmost operations column',
const cells = [...row.matchAll(/<(?:th|td)\b[^>]*>[\s\S]*?<\/(?:th|td)>/g)].map(([cell]) => cell);
assert.equal(cells.length, 7);
assert.match(cells[1], /class="column-title"/);
assert.doesNotMatch(cells[1], /<button|data-aksrt-msg-view|data-aksrt-msg-action/);
assert.doesNotMatch(cells[1], /<button|data-jio-msg-view|data-jio-msg-action/);
assert.match(cells[5], /class="column-time"/);
assert.match(cells[6], /class="column-actions"/);
assert.match(cells[6], /data-aksrt-msg-view=/);
assert.match(cells[6], /data-aksrt-msg-action="delete"/);
assert.match(cells[6], /data-jio-msg-view=/);
assert.match(cells[6], /data-jio-msg-action="delete"/);
});
assert.match(empty, /colspan="7"/);
const css = read('assets/css/admin/messages.css');
assert.match(css, /\.aksrt-messages-admin \.aksrt-messages-admin__table \.column-actions\s*\{[^}]*padding-left:\s*20px;[^}]*padding-right:\s*20px;[^}]*text-align:\s*center;/);
assert.match(css, /\.aksrt-msg-row-actions\s*\{[^}]*justify-content:\s*center;/);
assert.match(css, /\.jio-messages-admin \.jio-messages-admin__table \.column-actions\s*\{[^}]*padding-left:\s*20px;[^}]*padding-right:\s*20px;[^}]*text-align:\s*center;/);
assert.match(css, /\.jio-msg-row-actions\s*\{[^}]*justify-content:\s*center;/);
});
test('details preserve full content, escape HTML and unsafe links, and never mark read', () => {
const { table, cache_calls } = harness();
assert.match(table, /<template id="aksrt-msg-detail-8">/);
assert.match(table, /<template id="jio-msg-detail-8">/);
assert.match(table, /完整内容(?:完整内容){99}/);
assert.match(table, /&lt;script&gt;x&lt;\/script&gt;/);
assert.doesNotMatch(table, /<script>|href="javascript:/);
assert.match(table, /2026-10-30 11:30/);
assert.match(table, /查看详情不会改变收件人的已读状态/);
assert.equal((table.match(/data-aksrt-msg-action="mark_read" data-id=/g) || []).length, 1);
assert.equal((table.match(/data-jio-msg-action="mark_read" data-id=/g) || []).length, 1);
assert.deepEqual(cache_calls, [[7]]);
});
@@ -122,8 +122,8 @@ test('role choices display counts and disable only empty or oversized roles', ()
assert.doesNotMatch(compose, /value="contributor"[^>]* disabled/);
assert.match(compose, /data-count-limit="191"/);
assert.match(compose, /data-count-limit="2000"/);
assert.match(compose, /aria-describedby="aksrt-msg-content-count"/);
assert.match(compose, /data-aksrt-msg-pane="role" hidden/);
assert.match(compose, /aria-describedby="jio-msg-content-count"/);
assert.match(compose, /data-jio-msg-pane="role" hidden/);
});
test('overview links, status tabs, filter forms and mutations preserve period filtering', () => {
@@ -157,7 +157,7 @@ test('role broadcasts require confirmation of the current role and count on the
const { send, cap_checked, nonce_checked } = harness();
['unconfirmed', 'changed_count', 'changed_role', 'too_large', 'long_title'].forEach((key) => {
assert.equal(send[key].sent, 0, key);
assert.equal(send[key].redirect.aksrt_msg, 'send_failed', key);
assert.equal(send[key].redirect.jio_msg, 'send_failed', key);
});
assert.equal(send.confirmed.sent, 3);
assert.equal(send.users.sent, 1);
@@ -176,7 +176,7 @@ test('asset types use Chinese labels in filters, row badges and details', () =>
for (const [type, [label]] of Object.entries(asset_types)) {
assert.ok(filter_form.includes(`<option value="${type}">${label}</option>`));
}
assert.match(asset_table, /class="aksrt-msg-badge is-success"[^>]*>余额增加<\/a>/);
assert.match(asset_table, /class="jio-msg-badge is-success"[^>]*>余额增加<\/a>/);
assert.match(asset_table, /<dt>消息类型<\/dt><dd>余额增加<\/dd>/);
});
+13 -13
View File
@@ -1,11 +1,11 @@
<?php
/** WordPress-free message admin render / handler / SQL harness. No actual notifications are sent. */
namespace AKSRT\Core {
namespace Jio\Core {
final class DBMigrator {
public static function table($key) { return 'wp_aksrt_' . $key; }
public static function table($key) { return 'wp_jio_' . $key; }
}
}
namespace AKSRT\Messages {
namespace Jio\Messages {
final class Message {
public static $sent = [];
public static function get_instance() { return new self(); }
@@ -19,8 +19,8 @@ namespace {
define('ABSPATH', __DIR__);
define('ARRAY_A', 'ARRAY_A');
$root = dirname(__DIR__, 2);
define('AKSRT_DIR', $root);
define('AKSRT_URI', 'https://example.test/theme');
define('JIO_DIR', $root);
define('JIO_URI', 'https://example.test/theme');
class WP_Error {
public function __construct(public $code, public $message) {}
public function get_error_message() { return $this->message; }
@@ -97,7 +97,7 @@ namespace {
require $root . '/src/Admin/Native/UsersMenusMessagesCompose.php';
require $root . '/src/Admin/Native/UsersMenusMessages.php';
class Probe {
use \AKSRT\Admin\Native\UsersMenusMessages;
use \Jio\Admin\Native\UsersMenusMessages;
public $cap_checked = 0;
private function ensure_cap(): void { $this->cap_checked++; }
public function invoke($method, ...$args) { return $this->$method(...$args); }
@@ -122,18 +122,18 @@ namespace {
$out['cache_calls'] = $GLOBALS['cache_calls'];
$out['asset_table'] = $probe->html('render_message_table', [array_merge($row, ['type' => 'asset_balance_increase'])], $filters, 1, false);
foreach (['asset_balance_increase', 'asset_balance_decrease', 'asset_points_increase', 'asset_points_decrease'] as $type) {
$out['asset_types'][$type] = [\AKSRT\Messages\MessageTypes::label($type), \AKSRT\Messages\MessageTypes::tone($type)];
$out['asset_types'][$type] = [\Jio\Messages\MessageTypes::label($type), \Jio\Messages\MessageTypes::tone($type)];
}
$wpdb->prepared = [];
\AKSRT\Messages\NotificationAdminQuery::paginate($out['query_filters'], 1);
\AKSRT\Messages\NotificationAdminQuery::status_counts($out['query_filters']);
\AKSRT\Messages\NotificationAdminQuery::overview();
\Jio\Messages\NotificationAdminQuery::paginate($out['query_filters'], 1);
\Jio\Messages\NotificationAdminQuery::status_counts($out['query_filters']);
\Jio\Messages\NotificationAdminQuery::overview();
$out['sql'] = $wpdb->prepared;
$GLOBALS['zone'] = 'America/New_York';
$GLOBALS['now'] = '2026-11-01 12:00:00';
$wpdb->prepared = [];
\AKSRT\Messages\NotificationAdminQuery::overview();
\Jio\Messages\NotificationAdminQuery::overview();
$out['dst_sql'] = $wpdb->prepared;
$base = ['recipient_mode' => 'role', 'role' => 'subscriber', 'content' => 'Hello', 'title' => 'Title'];
@@ -149,10 +149,10 @@ namespace {
foreach ($cases as $name => $post) {
$_POST = $post;
$GLOBALS['role_count'] = $name === 'too_large' ? 501 : 3;
\AKSRT\Messages\Message::$sent = [];
\Jio\Messages\Message::$sent = [];
try { $probe->handle_send_message(); } catch (Redirect $redirect) {
parse_str(parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
$out['send'][$name] = ['redirect' => $query, 'sent' => count(\AKSRT\Messages\Message::$sent)];
$out['send'][$name] = ['redirect' => $query, 'sent' => count(\Jio\Messages\Message::$sent)];
}
}
$_POST = ['bulk_action' => 'delete', 'message_id' => 8, 'ids' => [9, 10], 'period' => 'today', 'status' => 'unread', 'paged' => 3];
@@ -55,7 +55,7 @@ function runtime({ autoopen = false, empty = false } = {}) {
const window = new Element();
doc.documentElement = new Element();
doc.getElementById = (id) => ids.get(id) || null;
doc.querySelector = (selector) => selector === '.aksrt-messages-admin' ? root : null;
doc.querySelector = (selector) => selector === '.jio-messages-admin' ? root : null;
const dialogs = [];
function dialog(id) {
const node = new Element();
@@ -65,23 +65,23 @@ function runtime({ autoopen = false, empty = false } = {}) {
node.cancel = () => { node.fire('cancel'); node.close(); };
node.closeButton = new Element();
node.closeButton.addEventListener('click', () => {});
node.map('[data-aksrt-dialog-close]', [node.closeButton]);
node.map('[data-jio-dialog-close]', [node.closeButton]);
ids.set(id, node);
dialogs.push(node);
return node;
}
const compose = dialog('aksrt-msg-compose');
const detail = dialog('aksrt-msg-detail');
const confirm = dialog('aksrt-msg-confirm');
const compose = dialog('jio-msg-compose');
const detail = dialog('jio-msg-detail');
const confirm = dialog('jio-msg-confirm');
confirm.attrs = { 'data-i18n-delete-title': '删除消息', 'data-i18n-delete-text': '删除 {count} 条消息', 'data-i18n-delete-send': '确认删除' };
const accept = new Element();
const cancel = confirm.closeButton;
const confirmation = new Element();
confirm.map('[data-aksrt-msg-confirm]', accept).map('[data-aksrt-dialog-title]', new Element()).map('#aksrt-msg-confirm-text', confirmation).map('[data-aksrt-dialog-close][autofocus]', cancel);
confirm.map('[data-jio-msg-confirm]', accept).map('[data-jio-dialog-title]', new Element()).map('#jio-msg-confirm-text', confirmation).map('[data-jio-dialog-close][autofocus]', cancel);
const detailBody = new Element();
detail.map('[data-aksrt-msg-detail-body]', detailBody);
ids.set('aksrt-msg-detail-8', { content: { cloneNode: () => ({ text: 'Full <script> message' }) } });
root.map('.aksrt-msg-dialog', dialogs);
detail.map('[data-jio-msg-detail-body]', detailBody);
ids.set('jio-msg-detail-8', { content: { cloneNode: () => ({ text: 'Full <script> message' }) } });
root.map('.jio-msg-dialog', dialogs);
const originalQuery = root.querySelector.bind(root);
root.querySelector = (selector) => selector === 'dialog[open]' ? dialogs.find((node) => node.open) || null : originalQuery(selector);
@@ -112,16 +112,16 @@ function runtime({ autoopen = false, empty = false } = {}) {
const list = form({ bulk_action: field('bulk_action'), message_id: field('message_id') }, checks);
const checkAll = new Element();
const selection = new Element({ 'data-label': '已选 %d 条' });
const bulkRead = new Element({ 'data-aksrt-msg-action': 'mark_read' });
const bulkDelete = new Element({ 'data-aksrt-msg-action': 'delete' });
const rowRead = new Element({ 'data-aksrt-msg-action': 'mark_read', 'data-id': '8' });
const rowDelete = new Element({ 'data-aksrt-msg-action': 'delete', 'data-id': '8' });
const view = new Element({ 'data-aksrt-msg-view': '8' });
const bulkRead = new Element({ 'data-jio-msg-action': 'mark_read' });
const bulkDelete = new Element({ 'data-jio-msg-action': 'delete' });
const rowRead = new Element({ 'data-jio-msg-action': 'mark_read', 'data-id': '8' });
const rowDelete = new Element({ 'data-jio-msg-action': 'delete', 'data-id': '8' });
const view = new Element({ 'data-jio-msg-view': '8' });
const actions = [bulkRead, bulkDelete, rowRead, rowDelete];
list.map('[data-aksrt-msg-check]', checks).map('[data-aksrt-msg-check-all]', checkAll).map('[data-aksrt-msg-selection]', selection)
.map('[data-aksrt-msg-action]:not([data-id])', [bulkRead, bulkDelete]).map('[data-aksrt-msg-action]', actions)
.map('button', [...actions, view]).map('button[data-id], button[data-aksrt-msg-view]', [rowRead, rowDelete, view]);
root.map('[data-aksrt-msg-list]', list).map('[data-aksrt-msg-view]', [view]);
list.map('[data-jio-msg-check]', checks).map('[data-jio-msg-check-all]', checkAll).map('[data-jio-msg-selection]', selection)
.map('[data-jio-msg-action]:not([data-id])', [bulkRead, bulkDelete]).map('[data-jio-msg-action]', actions)
.map('button', [...actions, view]).map('button[data-id], button[data-jio-msg-view]', [rowRead, rowDelete, view]);
root.map('[data-jio-msg-list]', list).map('[data-jio-msg-view]', [view]);
const role = field('role', '', true);
role.options = [new Element({ 'data-count': '0' }), new Element({ 'data-count': '3', 'data-label': '订阅者' })];
@@ -138,8 +138,8 @@ function runtime({ autoopen = false, empty = false } = {}) {
const usersRadio = new Element({ value: 'users' });
usersRadio.checked = true;
const roleRadio = new Element({ value: 'role' });
const usersPane = new Element({ 'data-aksrt-msg-pane': 'users' });
const rolePane = new Element({ 'data-aksrt-msg-pane': 'role' });
const usersPane = new Element({ 'data-jio-msg-pane': 'users' });
const rolePane = new Element({ 'data-jio-msg-pane': 'role' });
usersPane.map('input, select, textarea', [composeForm.elements.users]);
rolePane.map('input, select, textarea', [role]);
const titleCount = new Element();
@@ -148,15 +148,15 @@ function runtime({ autoopen = false, empty = false } = {}) {
const send = new Element();
composeForm.elements.title.attrs['data-count-limit'] = '191';
composeForm.elements.content.attrs['data-count-limit'] = '2000';
composeForm.map('[data-aksrt-msg-send]', send).map('[data-count-limit]', [composeForm.elements.title, composeForm.elements.content])
.map('[data-count-for="title"]', titleCount).map('[data-count-for="content"]', contentCount).map('[data-aksrt-msg-role-hint]', roleHint)
.map('[data-aksrt-msg-pane]', [usersPane, rolePane]).map('input[name="recipient_mode"]', [usersRadio, roleRadio]);
composeForm.map('[data-jio-msg-send]', send).map('[data-count-limit]', [composeForm.elements.title, composeForm.elements.content])
.map('[data-count-for="title"]', titleCount).map('[data-count-for="content"]', contentCount).map('[data-jio-msg-role-hint]', roleHint)
.map('[data-jio-msg-pane]', [usersPane, rolePane]).map('input[name="recipient_mode"]', [usersRadio, roleRadio]);
const formQuery = composeForm.querySelector.bind(composeForm);
composeForm.querySelector = (selector) => selector === 'input[name="recipient_mode"]:checked' ? (usersRadio.checked ? usersRadio : roleRadio) : formQuery(selector);
compose.map('[data-aksrt-msg-compose]', composeForm);
if (autoopen) compose.attrs['data-aksrt-dialog-autoopen'] = '';
compose.map('[data-jio-msg-compose]', composeForm);
if (autoopen) compose.attrs['data-jio-dialog-autoopen'] = '';
const trigger = new Element();
root.map('[data-aksrt-dialog-open]', [trigger]);
root.map('[data-jio-dialog-open]', [trigger]);
vm.runInNewContext(fs.readFileSync(path.resolve(__dirname, '../../assets/js/admin-messages.js'), 'utf8'), { document: doc, window });
function chooseRole() {
@@ -266,7 +266,7 @@ test('role sending uses a nested confirmation with the confirmed role and count'
r.cancel.fire('click');
assert.equal(r.compose.open, true);
assert.equal(r.composeForm.submissions.length, 0);
assert.equal(r.doc.documentElement.classList.contains('aksrt-msg-modal-open'), true);
assert.equal(r.doc.documentElement.classList.contains('jio-msg-modal-open'), true);
r.composeForm.requestSubmit(r.send); r.accept.fire('click');
assert.equal(r.composeForm.submissions.length, 1);
assert.equal(r.composeForm.submissions[0].confirmed_role, 'subscriber');
@@ -317,5 +317,5 @@ test('failed sends reopen compose and backdrop clicks must start outside the dia
r.compose.fire('pointerdown', { clientX: 0, clientY: 0 });
r.compose.fire('click', { clientX: 0, clientY: 0 });
assert.equal(r.compose.open, false);
assert.equal(r.doc.documentElement.classList.contains('aksrt-msg-modal-open'), false);
assert.equal(r.doc.documentElement.classList.contains('jio-msg-modal-open'), false);
});
@@ -12,18 +12,18 @@ test('message center exposes only the four configured-alias parameter-free route
const urls = read('inc/modules/user-parts/urls.php');
const registry = read('inc/modules/user-parts/template-registry.php');
assert.match(urls, /function aksrt_get_messages_rewrite_slug[\s\S]*?Settings::notification_settings\(\)[\s\S]*?\['slug'\]/);
assert.match(routing, /\$message_slug = preg_quote\(aksrt_get_messages_rewrite_slug\(\), '#'\)/);
assert.match(routing, /add_rewrite_rule\('\^' \. \$message_slug \. '\/\?\$',[\s\S]*?aksrt_message_view=unread/);
assert.match(urls, /function jio_get_messages_rewrite_slug[\s\S]*?Settings::notification_settings\(\)[\s\S]*?\['slug'\]/);
assert.match(routing, /\$message_slug = preg_quote\(jio_get_messages_rewrite_slug\(\), '#'\)/);
assert.match(routing, /add_rewrite_rule\('\^' \. \$message_slug \. '\/\?\$',[\s\S]*?jio_message_view=unread/);
assert.match(routing, /add_rewrite_rule\('\^' \. \$message_slug \. '\/\(posts\|like\|system\)\/\?\$'/);
assert.match(routing, /message-routing:' \. aksrt_get_messages_rewrite_slug\(\)/);
assert.match(routing, /message-routing:' \. jio_get_messages_rewrite_slug\(\)/);
assert.doesNotMatch(routing, /add_rewrite_rule\('\^message/);
assert.doesNotMatch(routing, /message\/\(\[0-9]|aksrt_message_id/);
assert.doesNotMatch(routing, /message\/\(\[0-9]|jio_message_id/);
const messageUrlFunction = urls.slice(
urls.indexOf('function aksrt_get_messages_page_url'),
urls.indexOf('endif;', urls.indexOf('function aksrt_get_messages_page_url')),
urls.indexOf('function jio_get_messages_page_url'),
urls.indexOf('endif;', urls.indexOf('function jio_get_messages_page_url')),
);
assert.match(messageUrlFunction, /aksrt_get_messages_rewrite_slug\(\)/);
assert.match(messageUrlFunction, /jio_get_messages_rewrite_slug\(\)/);
assert.doesNotMatch(messageUrlFunction, /add_query_arg|\/messages\/|\/message\//);
assert.doesNotMatch(registry, /'messages'\s*=>/);
assert.match(registry, /page-messages\.php'\)\s*\{[\s\S]*?return ''/);
@@ -77,7 +77,7 @@ test('message centre shows a skeleton during the AJAX load-more cycle', () => {
// The skeleton ships inside .messages-list so the loader is anchored to the
// end of the list, mirroring the reference post_ajax beforeSend hook.
assert.match(list, /aksrt_get_message_placeholder\(4\)/);
assert.match(list, /jio_get_message_placeholder\(4\)/);
assert.match(placeholder, /message-item--placeholder/);
assert.match(placeholder, /class="post_ajax_loader"/);
// The message skeleton reuses the shared .placeholder shimmer contract.
@@ -113,8 +113,8 @@ test('tabs, pagination, actions, and detail all replace DOM fragments', () => {
for (const label of ['未读消息', '文章评论', '点赞喜欢', '系统通知']) {
assert.ok(center.includes(label));
}
assert.match(center, /wp_ajax_aksrt_load_message_center/);
assert.match(center, /aksrt_render_message_center_detail/);
assert.match(center, /wp_ajax_jio_load_message_center/);
assert.match(center, /jio_render_message_center_detail/);
assert.match(template, /data-message-content/);
assert.match(script, /nav\.innerHTML = result\.data\.nav/);
assert.match(script, /content\.innerHTML = result\.data\.content/);
@@ -126,11 +126,11 @@ test('tabs, pagination, actions, and detail all replace DOM fragments', () => {
test('message detail exposes neither internal type labels nor a related-content link', () => {
const center = read('inc/modules/message-center.php');
const detailStart = center.indexOf('function aksrt_render_message_center_detail');
const detailEnd = center.indexOf("add_action('wp_ajax_aksrt_load_message_center'", detailStart);
const detailStart = center.indexOf('function jio_render_message_center_detail');
const detailEnd = center.indexOf("add_action('wp_ajax_jio_load_message_center'", detailStart);
const detail = center.slice(detailStart, detailEnd);
assert.doesNotMatch(detail, /aksrt_notification_type_label/);
assert.doesNotMatch(detail, /jio_notification_type_label/);
assert.doesNotMatch(detail, /查看相关内容|message-detail__primary|target_url|\['url'\]/);
assert.doesNotMatch(detail, /<a\s/i);
});
@@ -149,8 +149,8 @@ test('asset notifications are absent and interaction notification hooks are pres
]) {
assert.equal(combined.includes(retired), false, `${retired} should be absent`);
}
assert.doesNotMatch(notifications, /add_action\('aksrt_asset_transaction_committed'/);
for (const hook of ['aksrt_post_favorited', 'aksrt_post_liked', 'aksrt_comment_liked', 'aksrt_user_followed']) {
assert.doesNotMatch(notifications, /add_action\('jio_asset_transaction_committed'/);
for (const hook of ['jio_post_favorited', 'jio_post_liked', 'jio_comment_liked', 'jio_user_followed']) {
assert.ok(notifications.includes(`add_action('${hook}'`));
}
});
@@ -160,6 +160,6 @@ test('notification storage has no message-center legacy metadata fallback', () =
const service = read('src/Messages/Message.php');
const combined = `${notifications}\n${service}`;
assert.doesNotMatch(combined, /aksrt_notifications|aksrt_unread_message_count/);
assert.doesNotMatch(combined, /jio_notifications|jio_unread_message_count/);
assert.doesNotMatch(combined, /legacy|兼容|降级/i);
});
@@ -22,7 +22,7 @@ namespace {
function get_date_from_gmt($date, $format): string { return (string) $date; }
function add_action($hook, $callback): void {}
function aksrt_get_user_all_notifications(int $user_id): array {
function jio_get_user_all_notifications(int $user_id): array {
$items = [];
for ($number = 45; $number >= 1; $number--) {
$items[] = [
@@ -39,7 +39,7 @@ namespace {
}
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static string $paginateType = 'ajax_lists';
@@ -53,9 +53,9 @@ namespace {
require_once dirname(__DIR__, 2) . '/inc/modules/message-center.php';
function render_pagination_mode(string $type): string {
\AKSRT\Settings\Settings::$paginateType = $type;
\Jio\Settings\Settings::$paginateType = $type;
ob_start();
aksrt_render_message_center_list(1, 'unread', 2);
jio_render_message_center_list(1, 'unread', 2);
return (string) ob_get_clean();
}
@@ -6,7 +6,7 @@
namespace {
define('ABSPATH', __DIR__);
class AKSRT_User_Notifications {}
class JIO_User_Notifications {}
function sanitize_key($value): string {
return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? '';
@@ -17,7 +17,7 @@ namespace {
}
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function notification_settings(): array {
return ['slug' => 'inbox-center'];
@@ -33,10 +33,10 @@ namespace {
require_once dirname(__DIR__, 2) . '/inc/modules/user-parts/urls.php';
echo json_encode([
'slug' => aksrt_get_messages_rewrite_slug(),
'unread' => aksrt_get_messages_page_url(),
'posts' => aksrt_get_messages_page_url('posts'),
'like' => aksrt_get_messages_page_url('like'),
'system' => aksrt_get_messages_page_url('system'),
'slug' => jio_get_messages_rewrite_slug(),
'unread' => jio_get_messages_page_url(),
'posts' => jio_get_messages_page_url('posts'),
'like' => jio_get_messages_page_url('like'),
'system' => jio_get_messages_page_url('system'),
], JSON_UNESCAPED_SLASHES);
}
@@ -11,5 +11,5 @@ test('modal runtime compensates the removed scrollbar like Bootstrap setScrollba
assert.match(runtime, /scrollLock\.push\(padRight\(body, gap\)\)/);
assert.match(runtime, /var SCROLLBAR_FIXED = "\.nav-fixed \.header, \.float-right";/);
assert.match(runtime, /entry\.element\.style\.paddingRight = entry\.inline;/);
assert.doesNotMatch(runtime, /classList\.toggle\("aksrt-modal-runtime-open"/);
assert.doesNotMatch(runtime, /classList\.toggle\("jio-modal-runtime-open"/);
});
+22 -22
View File
@@ -28,10 +28,10 @@ class Element {
}
set innerHTML(value) {
this._innerHTML = value;
if (value.includes('aksrt-modal-runtime__content')) {
if (value.includes('jio-modal-runtime__content')) {
this.dialog = new Element(this.ownerDocument);
const content = new Element(this.ownerDocument);
content.className = 'aksrt-modal-runtime__content';
content.className = 'jio-modal-runtime__content';
content.parentNode = this;
this.content = content;
}
@@ -42,8 +42,8 @@ class Element {
getAttribute(name) { return this.attributes[name] || null; }
addEventListener(type, listener) { (this.listeners[type] ||= []).push(listener); }
querySelector(selector) {
if (selector === '.aksrt-modal-runtime__dialog') return this.dialog || null;
if (selector === '.aksrt-modal-runtime__content') return this.content || null;
if (selector === '.jio-modal-runtime__dialog') return this.dialog || null;
if (selector === '.jio-modal-runtime__content') return this.content || null;
return null;
}
querySelectorAll() { return []; }
@@ -92,16 +92,16 @@ function createRuntime() {
setTimeout(callback, delay) { callback.delay = delay; timers.push(callback); return timers.length; },
AbortController,
jQuery: jquery,
AksrtModalTelemetry: { start: () => '', mark() {} },
AksrtModalState: { create: () => ({}) },
AksrtModalCache: {
JioModalTelemetry: { start: () => '', mark() {} },
JioModalState: { create: () => ({}) },
JioModalCache: {
acquire(key, request) {
const promise = request();
return { promise, release() {} };
},
invalidate() {},
},
AksrtRequest: {
JioRequest: {
url: (url) => url,
request(url, options) {
const pending = {};
@@ -113,7 +113,7 @@ function createRuntime() {
};
const context = { window, document, CustomEvent: class { constructor(type, options) { this.type = type; this.detail = options?.detail; } }, Date, Promise, String, Number, Object, Array, Math, Error, AbortController };
vm.runInNewContext(fs.readFileSync(path.join(root, 'assets/js/core/modal-runtime.js'), 'utf8'), context);
return { runtime: window.AksrtModalRuntime, document, window, requests, timers, getSwipeConfig: () => swipeConfig };
return { runtime: window.JioModalRuntime, document, window, requests, timers, getSwipeConfig: () => swipeConfig };
}
function flushTimers(timers) {
@@ -130,11 +130,11 @@ test('nested remote dialogs preserve the parent, restore focus, and hold one pag
const childLoad = runtime.openRemote({ url: '/badge', cache: false, newModal: true });
const child = runtime.getActive();
assert.equal(parent.classList.contains('is-open'), true);
assert.equal(parent.classList.contains('aksrt-modal-runtime--parent'), true);
assert.equal(parent.classList.contains('jio-modal-runtime--parent'), true);
assert.equal(parent.getAttribute('aria-modal'), 'false');
assert.equal(parent.getAttribute('aria-hidden'), 'true');
assert.equal(child.getAttribute('aria-modal'), 'true');
assert.equal(document.documentElement.classList.contains('aksrt-modal-runtime-open'), true);
assert.equal(document.documentElement.classList.contains('jio-modal-runtime-open'), true);
requests[0].pending.resolve({ text: '<div>details</div>' });
await childLoad;
@@ -142,17 +142,17 @@ test('nested remote dialogs preserve the parent, restore focus, and hold one pag
flushTimers(timers);
assert.equal(runtime.getActive(), parent);
assert.equal(parent.classList.contains('aksrt-modal-runtime--parent'), false);
assert.equal(parent.classList.contains('jio-modal-runtime--parent'), false);
assert.equal(parent.getAttribute('aria-modal'), 'true');
assert.equal(parent.getAttribute('aria-hidden'), null);
assert.equal(parent.getAttribute('inert'), null);
assert.equal(document.activeElement, trigger);
assert.equal(document.documentElement.classList.contains('aksrt-modal-runtime-open'), true);
assert.equal(document.documentElement.classList.contains('jio-modal-runtime-open'), true);
runtime.close('control');
flushTimers(timers);
assert.equal(runtime.getActive(), null);
assert.equal(document.documentElement.classList.contains('aksrt-modal-runtime-open'), false);
assert.equal(document.documentElement.classList.contains('jio-modal-runtime-open'), false);
});
test('replacing a nested top dialog keeps the original parent and does not reload it', async () => {
@@ -170,7 +170,7 @@ test('replacing a nested top dialog keeps the original parent and does not reloa
assert.notEqual(secondChild, firstChild);
assert.equal(runtime.getActive(), secondChild);
assert.equal(parent.classList.contains('aksrt-modal-runtime--parent'), true);
assert.equal(parent.classList.contains('jio-modal-runtime--parent'), true);
assert.equal(requests.length, 2);
runtime.close('escape');
flushTimers(timers);
@@ -266,17 +266,17 @@ test('Escape from the child closes only that layer and aborts its request', asyn
test('remote trigger forwards the explicit new modal flag', () => {
const source = fs.readFileSync(path.join(root, 'assets/js/core/modal-triggers.js'), 'utf8');
const attrs = {
'data-aksrt-modal-url': '/badge',
'data-aksrt-modal-new': 'true',
'data-aksrt-modal-loading': 'spinner',
'data-aksrt-modal-swipe-close': 'true',
'data-jio-modal-url': '/badge',
'data-jio-modal-new': 'true',
'data-jio-modal-loading': 'spinner',
'data-jio-modal-swipe-close': 'true',
};
const element = { getAttribute: (name) => attrs[name] || null };
const document = {
addEventListener(type, listener) { this.listener = listener; },
};
let options;
const window = { AksrtModalRuntime: { openRemote(value) { options = value; } } };
const window = { JioModalRuntime: { openRemote(value) { options = value; } } };
vm.runInNewContext(source, { window, document, parseInt, Object });
document.listener({
target: { closest: () => element },
@@ -293,11 +293,11 @@ test('bottom dialogs use the shared spinner and minitouch swipe handle', () => {
const shell = runtime.open({ bottom: true, swipeClose: true, loading: 'spinner' });
const dialog = shell.innerHTML;
assert.match(dialog, /class="aksrt-modal-runtime__loading flex jc"/);
assert.match(dialog, /class="jio-modal-runtime__loading flex jc"/);
assert.match(dialog, /class="loading-circle" viewBox="0 0 50 50"/);
assert.match(dialog, /class="touch-close" aria-hidden="true"/);
assert.equal(getSwipeConfig().direction, 'bottom');
assert.equal(getSwipeConfig().selector, '.aksrt-modal-runtime__dialog');
assert.equal(getSwipeConfig().selector, '.jio-modal-runtime__dialog');
assert.equal(getSwipeConfig().start_selector, '.modal-colorful-header,.touch-close,.touch');
assert.equal(getSwipeConfig().depreciation, 50);
@@ -8,14 +8,14 @@ const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('message notification settings are a canonical user/msg inline panel', () => {
const center = read('src/User/Center.php');
const helpers = read('inc/helpers/aksrt-user-center.php');
const helpers = read('inc/helpers/jio-user-center.php');
const sidebar = read('inc/helpers/user-center-renderers/sidebar-nav.php');
const routing = read('inc/modules/user-parts/routing.php');
assert.match(center, /'msg'\s*=>\s*\[[\s\S]*?'type'\s*=>\s*'panel'/);
assert.doesNotMatch(center, /'notif'\s*=>/);
assert.match(helpers, /'msg'\s*=>\s*'msg'/);
assert.match(sidebar, /aksrt_get_user_center_tab_url\('msg'\)/);
assert.match(sidebar, /jio_get_user_center_tab_url\('msg'\)/);
assert.match(sidebar, /'tab'\s*=>\s*'msg'/);
assert.doesNotMatch(sidebar, /'url'\s*=>\s*\(string\) \(\$messages_url/);
assert.match(routing, /'revenue', 'msg', 'settings'/);
@@ -25,10 +25,10 @@ test('notification icon positions map to their four render locations', () => {
const phpSchema = read('src/Admin/Schema/SchemaUsers.php');
const jsSchema = read('assets/admin/src/schema/sections/users/notifications.js');
const integration = read('src/Settings/Integration.php');
const header = read('inc/helpers/header/aksrt-header-navbar.php');
const header = read('inc/helpers/header/jio-header-navbar.php');
const headerStyles = read('assets/css/frontend/header-userbox.css');
const legacyMainCss = read('assets/css/main.css');
const userCenter = read('inc/helpers/aksrt-user-center.php');
const userCenter = read('inc/helpers/jio-user-center.php');
const userHome = read('templates/parts/user/author-hero.php');
for (const source of [phpSchema, jsSchema]) {
@@ -47,8 +47,8 @@ test('notification icon positions map to their four render locations', () => {
assert.match(header, /toggle-radius msg-icon/);
assert.match(header, /<badge class="top">/);
assert.match(legacyMainCss, /\.sub-user-box \.msg-news-icon \{[\s\S]*?top: 0\.4em;[\s\S]*?right: 0;/);
assert.doesNotMatch(headerStyles, /\.aksrt-header-userbox \.user-info > \.abs-right/);
assert.match(header, /aksrt-nav-icon aksrt-nav-notification/);
assert.doesNotMatch(headerStyles, /\.jio-header-userbox \.user-info > \.abs-right/);
assert.match(header, /jio-nav-icon jio-nav-notification/);
assert.match(header, /\$notification_but \. \$sign_but \. \$search_but/);
assert.match(userCenter, /in_array\('user_center_header'/);
assert.match(userHome, /in_array\('user_home_header'/);
@@ -57,9 +57,9 @@ test('notification icon positions map to their four render locations', () => {
test('notification preference endpoint stores only the three boolean categories', () => {
const controller = read('src/User/NotificationPreferences.php');
assert.match(controller, /META_KEY = 'aksrt_notification_preferences'/);
assert.match(controller, /META_KEY = 'jio_notification_preferences'/);
assert.match(controller, /CATEGORIES = \['posts', 'like', 'system'\]/);
assert.match(controller, /register_rest_route\('aksrt\/v1', '\/user\/notification-preferences'/);
assert.match(controller, /register_rest_route\('jio\/v1', '\/user\/notification-preferences'/);
assert.match(controller, /permission_callback/);
assert.match(controller, /current_user_can\('read'\)/);
assert.match(controller, /'additionalProperties'\s*=>\s*false/);
@@ -85,7 +85,7 @@ test('notification preference UI uses the global toast lifecycle', () => {
assert.match(template, /role="switch"/);
assert.match(styles, /\.uc-notification-switch input\[type='checkbox'\][\s\S]*?opacity: 0 !important/);
assert.match(styles, /\.uc-notification-switch input\[type='checkbox'\]::before,[\s\S]*?\.uc-notification-switch input\[type='checkbox'\]::after[\s\S]*?content: none !important/);
assert.match(script, /aksrt-notification-preferences-toast/);
assert.match(script, /jio-notification-preferences-toast/);
assert.match(script, /notyf\([^;]+?'load'[^;]+?toastId/);
assert.match(script, /notyf\([^;]+?'success'[^;]+?toastId/);
assert.match(script, /notyf\([^;]+?'danger'[^;]+?toastId/);
@@ -130,7 +130,7 @@ test('OAuth bind starts require a one-time captcha bind pass', () => {
assert.match(oauth, /validate_bind_pass\(\$bind_pass, \$provider, \$user_id, true\)/)
assert.match(oauth, /validate_bind_pass\(\$bind_pass, 'clogin_' \. \$type, get_current_user_id\(\), true\)/)
assert.match(weixingzh, /OAuth::validate_bind_pass\(\$bind_pass, 'weixingzh'/)
assert.match(bindings, /aksrt_render_captcha\('bind'\)/)
assert.match(bindings, /jio_render_captcha\('bind'\)/)
})
test('OAuth result handler maps safe errors and preserves unrelated URL state', () => {
@@ -156,7 +156,7 @@ test('OAuth result handler maps safe errors and preserves unrelated URL state',
}
const jQueryStub = () => jQueryApi
const window = {
aksrtData: {},
jioData: {},
URL,
location,
history: { replaceState: (_state, _title, value) => { replaced = value } },
+37 -37
View File
@@ -16,7 +16,7 @@ $GLOBALS['wp_options'] = [];
$GLOBALS['wp_cache'] = [];
$GLOBALS['captcha_enabled'] = true;
function aksrt_is_captcha_enabled_for(string $scene): bool { return $scene === 'bind' && !empty($GLOBALS['captcha_enabled']); }
function jio_is_captcha_enabled_for(string $scene): bool { return $scene === 'bind' && !empty($GLOBALS['captcha_enabled']); }
function __(string $text, string $domain = ''): string { return $text; }
function sanitize_key(string $value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($value)) ?? ''; }
function sanitize_text_field(string $value): string { return trim(strip_tags($value)); }
@@ -76,7 +76,7 @@ final class HarnessWpdb {
$GLOBALS['wpdb'] = new HarnessWpdb();
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function flush_settings_cache(): void {}
public static function sync_native_site_icon(string $value): void {}
@@ -99,51 +99,51 @@ namespace {
if (!$condition) throw new RuntimeException($message);
}
$_COOKIE['aksrt_oauth_browser'] = str_repeat('A', 43);
$token = \AKSRT\Auth\OAuthTransactionStore::issue('direct', ['provider' => 'github'], 60, true);
check(\AKSRT\Auth\OAuthTransactionStore::peek($token, 'direct', true) !== null, 'transaction peek failed');
check(\AKSRT\Auth\OAuthTransactionStore::peek($token, 'other', true) === null, 'transaction kind mismatch accepted');
$_COOKIE['aksrt_oauth_browser'] = str_repeat('B', 43);
check(\AKSRT\Auth\OAuthTransactionStore::claim($token, 'direct', true) === null, 'wrong browser claimed transaction');
$_COOKIE['aksrt_oauth_browser'] = str_repeat('A', 43);
check(\AKSRT\Auth\OAuthTransactionStore::claim($token, 'direct', true) !== null, 'valid browser failed claim');
check(\AKSRT\Auth\OAuthTransactionStore::claim($token, 'direct', true) === null, 'transaction replay accepted');
$_COOKIE['jio_oauth_browser'] = str_repeat('A', 43);
$token = \Jio\Auth\OAuthTransactionStore::issue('direct', ['provider' => 'github'], 60, true);
check(\Jio\Auth\OAuthTransactionStore::peek($token, 'direct', true) !== null, 'transaction peek failed');
check(\Jio\Auth\OAuthTransactionStore::peek($token, 'other', true) === null, 'transaction kind mismatch accepted');
$_COOKIE['jio_oauth_browser'] = str_repeat('B', 43);
check(\Jio\Auth\OAuthTransactionStore::claim($token, 'direct', true) === null, 'wrong browser claimed transaction');
$_COOKIE['jio_oauth_browser'] = str_repeat('A', 43);
check(\Jio\Auth\OAuthTransactionStore::claim($token, 'direct', true) !== null, 'valid browser failed claim');
check(\Jio\Auth\OAuthTransactionStore::claim($token, 'direct', true) === null, 'transaction replay accepted');
$bindPass = \AKSRT\Auth\OAuthTransactionStore::issue('oauth_bind_pass', ['user_id' => 17, 'provider' => 'github'], 300, true);
check(\AKSRT\Auth\OAuth::validate_bind_pass($bindPass, 'github', 17, false), 'valid bind pass peek failed');
check(!\AKSRT\Auth\OAuth::validate_bind_pass($bindPass, 'gitee', 17, true), 'wrong bind provider consumed as valid');
check(!\AKSRT\Auth\OAuth::validate_bind_pass($bindPass, 'github', 18, true), 'wrong bind user consumed as valid');
check(\AKSRT\Auth\OAuth::validate_bind_pass($bindPass, 'github', 17, true), 'valid bind pass claim failed after rejected mismatches');
check(!\AKSRT\Auth\OAuth::validate_bind_pass($bindPass, 'github', 17, true), 'bind pass replay accepted');
$bindPass = \Jio\Auth\OAuthTransactionStore::issue('oauth_bind_pass', ['user_id' => 17, 'provider' => 'github'], 300, true);
check(\Jio\Auth\OAuth::validate_bind_pass($bindPass, 'github', 17, false), 'valid bind pass peek failed');
check(!\Jio\Auth\OAuth::validate_bind_pass($bindPass, 'gitee', 17, true), 'wrong bind provider consumed as valid');
check(!\Jio\Auth\OAuth::validate_bind_pass($bindPass, 'github', 18, true), 'wrong bind user consumed as valid');
check(\Jio\Auth\OAuth::validate_bind_pass($bindPass, 'github', 17, true), 'valid bind pass claim failed after rejected mismatches');
check(!\Jio\Auth\OAuth::validate_bind_pass($bindPass, 'github', 17, true), 'bind pass replay accepted');
$GLOBALS['captcha_enabled'] = false;
check(\AKSRT\Auth\OAuth::validate_bind_pass('', 'github', 17, true), 'disabled bind captcha required a pass');
check(\Jio\Auth\OAuth::validate_bind_pass('', 'github', 17, true), 'disabled bind captcha required a pass');
$GLOBALS['captcha_enabled'] = true;
$expired = \AKSRT\Auth\OAuthTransactionStore::issue('direct', [], 30, true);
$expired = \Jio\Auth\OAuthTransactionStore::issue('direct', [], 30, true);
foreach ($GLOBALS['wp_options'] as $key => $row) {
if (is_array($row) && ($row['kind'] ?? '') === 'direct') $GLOBALS['wp_options'][$key]['expires_at'] = time() - 1;
}
check(\AKSRT\Auth\OAuthTransactionStore::peek($expired, 'direct', true) === null, 'expired transaction accepted');
check(\Jio\Auth\OAuthTransactionStore::peek($expired, 'direct', true) === null, 'expired transaction accepted');
$request = [
'v' => '2', 'request_id' => str_repeat('C', 43), 'provider' => 'github', 'action' => 'login',
'agent_back_url' => 'https://example.com/oauth/proxy/callback/', 'ts' => (string) time(),
];
$secret = 'test-secret-value';
$requestSign = \AKSRT\Auth\OAuthProxyProtocol::sign_request($request, $secret);
check(\AKSRT\Auth\OAuthProxyProtocol::verify_request($request, $secret, $requestSign), 'request signature failed');
check(!\AKSRT\Auth\OAuthProxyProtocol::verify_callback($request, $secret, $requestSign), 'direction-separated HMAC failed');
$requestSign = \Jio\Auth\OAuthProxyProtocol::sign_request($request, $secret);
check(\Jio\Auth\OAuthProxyProtocol::verify_request($request, $secret, $requestSign), 'request signature failed');
check(!\Jio\Auth\OAuthProxyProtocol::verify_callback($request, $secret, $requestSign), 'direction-separated HMAC failed');
$tampered = $request; $tampered['provider'] = 'qq';
check(!\AKSRT\Auth\OAuthProxyProtocol::verify_request($tampered, $secret, $requestSign), 'tampered request accepted');
check(!\AKSRT\Auth\OAuthProxyProtocol::timestamp_valid((string) (time() - 1000)), 'expired timestamp accepted');
check(!\Jio\Auth\OAuthProxyProtocol::verify_request($tampered, $secret, $requestSign), 'tampered request accepted');
check(!\Jio\Auth\OAuthProxyProtocol::timestamp_valid((string) (time() - 1000)), 'expired timestamp accepted');
check(\AKSRT\Auth\OAuthUrlPolicy::public_https('http://example.com') === '', 'HTTP URL accepted');
check(\AKSRT\Auth\OAuthUrlPolicy::public_https('https://user@example.com') === '', 'userinfo URL accepted');
check(\AKSRT\Auth\OAuthUrlPolicy::public_https('https://127.0.0.1') === '', 'loopback URL accepted');
check(\AKSRT\Auth\OAuthUrlPolicy::public_https('https://10.0.0.1') === '', 'private URL accepted');
check(\AKSRT\Auth\OAuthProxyProtocol::callback_url('https://example.com/wrong') === '', 'wrong callback path accepted');
check(\Jio\Auth\OAuthUrlPolicy::public_https('http://example.com') === '', 'HTTP URL accepted');
check(\Jio\Auth\OAuthUrlPolicy::public_https('https://user@example.com') === '', 'userinfo URL accepted');
check(\Jio\Auth\OAuthUrlPolicy::public_https('https://127.0.0.1') === '', 'loopback URL accepted');
check(\Jio\Auth\OAuthUrlPolicy::public_https('https://10.0.0.1') === '', 'private URL accepted');
check(\Jio\Auth\OAuthProxyProtocol::callback_url('https://example.com/wrong') === '', 'wrong callback path accepted');
$provider = \AKSRT\Admin\SchemaProvider::get_instance();
$provider = \Jio\Admin\SchemaProvider::get_instance();
$base = $provider->get_defaults('users', 'oauth');
$base['oauth_s'] = true;
$base['oauth_weixingzh_s'] = true;
@@ -151,18 +151,18 @@ namespace {
$base['weixingzh_appid'] = 'appid';
$base['weixingzh_appsecret'] = 'secret';
$base['weixingzh_token'] = '***';
$GLOBALS['wp_options']['aksrt_settings'] = [];
$GLOBALS['wp_options']['jio_settings'] = [];
check(!$provider->validate('users', $base, 'oauth')['valid'], 'masked missing token treated as configured');
$GLOBALS['wp_options']['aksrt_settings'] = ['weixingzh_token' => 'stored-token'];
$GLOBALS['wp_options']['jio_settings'] = ['weixingzh_token' => 'stored-token'];
check($provider->validate('users', $base, 'oauth')['valid'], 'stored token not preserved by mask validation');
$provider->write_settings('users', ['weixingzh_token' => '***'], 'oauth');
check($GLOBALS['wp_options']['aksrt_settings']['weixingzh_token'] === 'stored-token', 'masked token overwrote stored secret');
check($GLOBALS['wp_options']['jio_settings']['weixingzh_token'] === 'stored-token', 'masked token overwrote stored secret');
$provider->write_settings('users', ['weixingzh_token' => 'replacement'], 'oauth');
check($GLOBALS['wp_options']['aksrt_settings']['weixingzh_token'] === 'replacement', 'new token failed to replace secret');
check($GLOBALS['wp_options']['jio_settings']['weixingzh_token'] === 'replacement', 'new token failed to replace secret');
$provider->write_settings('users', ['__clear_secrets' => ['weixingzh_token']], 'oauth');
check(!isset($GLOBALS['wp_options']['aksrt_settings']['weixingzh_token']), 'explicit secret clear failed');
$GLOBALS['wp_options']['aksrt_settings']['weixingzh_token'] = 'final-token';
check(!isset($GLOBALS['wp_options']['jio_settings']['weixingzh_token']), 'explicit secret clear failed');
$GLOBALS['wp_options']['jio_settings']['weixingzh_token'] = 'final-token';
$clearData = $base;
$clearData['weixingzh_token'] = '***';
$clearData['__clear_secrets'] = ['weixingzh_token'];
@@ -18,15 +18,15 @@ const settingsFacade = read('src/Settings/Settings.php')
const scriptsPart1c = read('src/Core/ScriptsPart1c.php')
const footerCss = read('assets/css/frontend/footer/shell.css')
const frontendBaseCss = read('assets/css/frontend/base.css')
const headerMenuButtons = read('inc/helpers/aksrt-header-menu-buttons.php')
const headerMenuButtons = read('inc/helpers/jio-header-menu-buttons.php')
const frontendRenderer = read('src/WidgetSystem/FrontendRenderer.php')
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php')
const widgetCss = read('assets/widgets/frontend.css')
const headerNavbar = read('inc/helpers/header/aksrt-header-navbar.php')
const headerMobile = read('inc/helpers/header/aksrt-header-mobile.php')
const headerRender = read('inc/helpers/header/aksrt-header-render.php')
const headerUserbox = read('inc/helpers/aksrt-header-userbox.php')
const headerSearch = read('inc/helpers/header/aksrt-header-menu-search.php')
const headerNavbar = read('inc/helpers/header/jio-header-navbar.php')
const headerMobile = read('inc/helpers/header/jio-header-mobile.php')
const headerRender = read('inc/helpers/header/jio-header-render.php')
const headerUserbox = read('inc/helpers/jio-header-userbox.php')
const headerSearch = read('inc/helpers/header/jio-header-menu-search.php')
const headerCss = read('assets/css/frontend/header-userbox.css')
const coreResetCss = read('assets/css/core/reset.css')
const vipCardRenderer = read('src/Content/VipServiceModal.php')
@@ -119,7 +119,7 @@ test('defaults, nested structures, dependencies, and excluded product families a
assert.equal(schema.includes(dead), false, `legacy/excluded field remains: ${dead}`)
}
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
assert.match(headerMenuButtons, /jio_render_new_add_btns\(\['post'\], \$class, \$label\)/)
// FA5 spelling of the reference '<i class="fa fa-fw fa-pencil"></i>发布'
// default: fa-pencil does not exist in the bundled FA 5.15.4 free set.
assert.match(schema, /'nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"><\/i>发布'/)
@@ -150,13 +150,13 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
assert.match(headerMenuButtons, /'<a rel="nofollow" class="newadd-btns ' \. esc_attr\(\$class\) \. ' btn-newadd"/)
assert.equal((headerMenuButtons.match(/Settings::close_sign\(\)/g) || []).length, 2)
for (const dead of ['navbar-cta', 'navbar-cta-text', 'navbar-newposts', 'navbar-vip-hover', 'newadd-btns__', 'vip-level', 'fa-gem']) {
assert.equal(headerMenuButtons.includes(dead), false, `AKSRT-only header CTA token remains: ${dead}`)
assert.equal(headerMenuButtons.includes(dead), false, `Jio-only header CTA token remains: ${dead}`)
}
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
assert.match(vipCardRenderer, /jio_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
assert.match(vipCardRenderer, /<div class=\"vip-name mb10\"><span class=\"mr6\">/)
assert.match(vipCardRenderer, /aksrt_format_vip_equity_html\(\$equity, 'mb10 relative'\)/)
assert.match(vipCardRenderer, /jio_format_vip_equity_html\(\$equity, 'mb10 relative'\)/)
assert.doesNotMatch(vipCardRenderer, /vip-name__(?:icon|label)/)
assert.doesNotMatch(vipModalCss, /Header hover VIP cards/)
@@ -178,9 +178,9 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
test('user card VIP CTA matches the reference zib_get_header_payvip_icon()', () => {
assert.match(headerUserbox, /'<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'/)
assert.match(headerUserbox, /<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
assert.match(headerUserbox, /esc_html__\('开通会员 尊享会员权益', 'aksrt'\)/)
assert.match(headerUserbox, /esc_html__\('开通会员 尊享会员权益', 'jio'\)/)
for (const dead of ['fa-gem', 'vip-level']) {
assert.equal(headerUserbox.includes(dead), false, `AKSRT-only user card CTA token remains: ${dead}`)
assert.equal(headerUserbox.includes(dead), false, `Jio-only user card CTA token remains: ${dead}`)
}
assert.doesNotMatch(headerUserbox, /<span>' \. esc_html__\('开通会员 尊享会员权益'/)
})
@@ -195,10 +195,10 @@ test('VIP purchase modal matches legacy desktop-center and mobile-bottom positio
// 高度/滚动 = .modal.flex .modal-content 的 max-height calc(100vh - 20px)
// + overflow-y auto(阈值放宽到整屏,正常内容不出现滚动条);
// 圆角 = .modal-content 的 var(--main-radius)(8px)。
assert.match(vipModalCss, /\.aksrt-vip-modal-shell \.aksrt-modal-runtime__dialog\s*\{[\s\S]*?width:\s*min\(600px, calc\(100vw - 20px\)\);[\s\S]*?max-height:\s*calc\(100vh - 20px\);[\s\S]*?overflow:\s*hidden;[\s\S]*?overflow-y:\s*auto;[\s\S]*?border-radius:\s*var\(--main-radius, 8px\);/)
assert.match(vipModalCss, /\.jio-vip-modal-shell \.jio-modal-runtime__dialog\s*\{[\s\S]*?width:\s*min\(600px, calc\(100vw - 20px\)\);[\s\S]*?max-height:\s*calc\(100vh - 20px\);[\s\S]*?overflow:\s*hidden;[\s\S]*?overflow-y:\s*auto;[\s\S]*?border-radius:\s*var\(--main-radius, 8px\);/)
// 内边距由 .box-body 15px 提供(参考主题无 .payvip-modal 专属 padding)。
assert.doesNotMatch(vipModalCss, /\.payvip-modal\s*\{[^}]*padding:/)
assert.match(vipModalCss, /@media \(max-width: 768px\)[\s\S]*?\.aksrt-vip-modal-shell\.aksrt-modal-runtime--bottom \.aksrt-modal-runtime__dialog\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-height:\s*calc\(100vh - 20px\);[\s\S]*?border-radius:\s*var\(--main-radius, 8px\) var\(--main-radius, 8px\) 0 0;/)
assert.match(vipModalCss, /@media \(max-width: 768px\)[\s\S]*?\.jio-vip-modal-shell\.jio-modal-runtime--bottom \.jio-modal-runtime__dialog\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-height:\s*calc\(100vh - 20px\);[\s\S]*?border-radius:\s*var\(--main-radius, 8px\) var\(--main-radius, 8px\) 0 0;/)
})
test('schema REST saves are privileged, section-whitelisted, and deeply sanitized', () => {
@@ -231,8 +231,8 @@ test('legacy routes, components, and storage paths are gone', () => {
const repository = activeRepositorySource()
for (const dead of [
'aksrt_footer_settings', 'aksrt_banners', 'aksrt_banner', 'aksrt-banner', 'HomeModules', 'homepage_modules',
'/settings/homepage', 'home-modules', 'cms-home-modules', 'aksrt_preview', 'theme-background',
'jio_footer_settings', 'jio_banners', 'jio_banner', 'jio-banner', 'HomeModules', 'homepage_modules',
'/settings/homepage', 'home-modules', 'cms-home-modules', 'jio_preview', 'theme-background',
"templates/parts/home-modules",
]) {
assert.equal(repository.includes(dead), false, `active legacy implementation remains: ${dead}`)
@@ -278,7 +278,7 @@ test('front-end page-display consumers read only the canonical section contracts
test('footer brand icons keep the Brands font when legacy fa prefix is present', () => {
const footer = read('footer.php')
const icons = read('inc/helpers/aksrt-icons.php')
const icons = read('inc/helpers/jio-icons.php')
assert.match(footer, /class="fa fab fa-weixin"/)
assert.match(footer, /class="fa fab fa-qq"/)
@@ -301,8 +301,8 @@ test('header multifunction search keeps focus and submit alignment route-indepen
test('user profile and author widgets render the reference user-card contract', () => {
assert.match(frontendRenderer, /in_array\(\$definition->slug\(\), \['user_card', 'author_card'\], true\)/)
assert.match(frontendRenderer, /self::render_reference_widget\(\$definition, \$instance, \$widget, \$content\)/)
assert.match(frontendRenderer, /\$classes = \['zib-widget-wrap', 'aksrt-'/)
assert.match(frontendRenderer, /\$classes\[\] = 'aksrt-user-widget-wrap'/)
assert.match(frontendRenderer, /\$classes = \['zib-widget-wrap', 'jio-'/)
assert.match(frontendRenderer, /\$classes\[\] = 'jio-user-widget-wrap'/)
assert.match(frontendRenderer, /echo '<div class="widget-container"><div>';/)
assert.match(frontendRenderer, /\$content_classes = \['widget-content'\]/)
// 登录卡片与 reference zib_get_user_card_box 同构:封面、头像框、名字行、统计徽章、简介、按钮。
@@ -314,17 +314,17 @@ test('user profile and author widgets render the reference user-card contract',
assert.match(userRenderer, /author-tag mt10 mini-scrollbar/)
assert.match(userRenderer, /user-desc mt10 muted-2-color em09/)
assert.match(userRenderer, /user-btns mt20/)
assert.match(userRenderer, /aksrt_get_user_badges\(\$user_id\)/)
assert.match(userRenderer, /jio_get_user_badges\(\$user_id\)/)
assert.match(userRenderer, /'follow' => true,/)
// 用户个人信息小工具不显示文章模块,显示签到徽章。
assert.match(userRenderer, /'show_posts' => false,/)
assert.match(userRenderer, /'show_checkin' => true,/)
assert.match(userRenderer, /aksrt_header_user_checkin_btn\(\$user_id, 'img-badge jb-yellow'\)/)
assert.match(userRenderer, /jio_header_user_checkin_btn\(\$user_id, 'img-badge jb-yellow'\)/)
// 文章作者信息小工具显示文章模块,并走同一 widget 变体。
assert.match(userRenderer, /'show_posts' => !isset\(\$i\['show_posts'\]\) \|\| !empty\(\$i\['show_posts'\]\),/)
assert.match(userRenderer, /'post_type' => 'post',/)
assert.match(userRenderer, /self::card_box\(\(int\) \$user->ID,[\s\S]*?\) \. '<\/div>';/)
assert.match(userRenderer, /aksrt_render_user_card_posts\(/)
assert.match(userRenderer, /jio_render_user_card_posts\(/)
assert.match(userRenderer, /'post_type' => \(string\) \$args\['post_type'\],/)
// 未登录卡片使用主题认证真值和 reference 登录/社交 DOM。
assert.match(userRenderer, /Settings::close_sign\(\)/)
@@ -346,26 +346,26 @@ test('user profile and author widgets render the reference user-card contract',
assert.doesNotMatch(widgetCss, /\.widget-content[^{,]*\{[^}]*padding/s)
assert.doesNotMatch(read('assets/css/frontend/sidebar.css'), /\.widget-content[^{,]*\{[^}]*padding/s)
assert.doesNotMatch(widgetCss, /user-card__auth-btns/)
assert.doesNotMatch(widgetCss, /\.aksrt-user-card\s*\{/)
assert.doesNotMatch(userRenderer, /aksrt-widget-button is-ghost/)
assert.doesNotMatch(widgetCss, /\.jio-user-card\s*\{/)
assert.doesNotMatch(userRenderer, /jio-widget-button is-ghost/)
})
test('desktop header actions share one row without generic button masks', () => {
assert.match(headerNavbar, /navbar-form navbar-right navbar-but/)
assert.match(headerNavbar, /navbar-form navbar-right flex ac/)
assert.match(headerNavbar, /\$fold \. \('3' === \$layout \? \$right_but \. \$button : \$button \. \$right_but\)/)
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
assert.match(headerNavbar, /class="jio-nav-icon nav-search-btn main-search-btn"/)
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
assert.match(headerNavbar, /<ul class="list-inline splitters relative">/)
assert.doesNotMatch(headerNavbar, /aksrt-navbar-account|aksrt-nav-avatar/)
assert.doesNotMatch(headerNavbar, /jio-navbar-account|jio-nav-avatar/)
assert.match(headerNavbar, /class="navbar-avatar"/)
assert.match(legacyMainCss, /\.navbar-avatar\s*\{[^}]*margin-top:\s*3px;[^}]*width:\s*29px;[^}]*height:\s*29px;/s)
assert.match(headerNavbar, /aksrt_get_translate_language_btn\('ml10 pull-right'\)/)
assert.match(headerNavbar, /jio_get_translate_language_btn\('ml10 pull-right'\)/)
assert.match(headerNavbar, /<a data-toggle-class data-target="\.nav\.navbar-nav" href="javascript:;" class="but"/)
assert.match(headerNavbar, /<svg class="icon" viewBox="50 0 924 924"/)
assert.match(headerNavbar, /<svg class="icon" aria-hidden="true"><use xlink:href="#icon-search"/)
assert.doesNotMatch(headerCss, /\.aksrt-navbar-account|\.aksrt-nav-avatar|navbar-form\.flex\.ac\s*\{[^}]*min-height/)
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s)
assert.doesNotMatch(headerCss, /\.jio-navbar-account|\.jio-nav-avatar|navbar-form\.flex\.ac\s*\{[^}]*min-height/)
assert.match(headerCss, /\.navbar-top \.jio-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s)
// The publish and VIP chips ride the legacy .but geometry verbatim; nothing in
// the header module may restate their padding, margin, radius or icon size.
// Descendant, not child: the VIP chip lives inside the hover-show wrapper.
@@ -373,7 +373,7 @@ test('desktop header actions share one row without generic button masks', () =>
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
// No merged flex container: the three groups keep their own .navbar-form
// padding, which is what produces the reference inter-cluster spacing.
assert.doesNotMatch(headerCss, /\.aksrt-navbar-actions|\.aksrt-navbar-account|\.aksrt-nav-avatar/)
assert.doesNotMatch(headerCss, /\.jio-navbar-actions|\.jio-navbar-account|\.jio-nav-avatar/)
})
test('mobile search follows the reference center and left header placements', () => {
@@ -381,15 +381,15 @@ test('mobile search follows the reference center and left header placements', ()
assert.match(headerNavbar, /mobile_layout/)
assert.match(headerNavbar, /'center' === \(\$ctx\['mobile_layout'\] \?\? 'center'\)/)
assert.match(headerMobile, /main-search-btn|toggle-radius/)
assert.match(headerMobile, /aksrt_nav_mobile_content\(\$ctx\)/)
assert.match(headerMobile, /jio_nav_mobile_content\(\$ctx\)/)
assert.doesNotMatch(headerRender, /'fixed'\s*=>/)
assert.doesNotMatch(legacyMainCss, /\.mobile-navbar \.navbar-(?:header|brand)/)
assert.doesNotMatch(coreResetCss, /img,\s*picture,\s*video,\s*canvas,\s*svg\s*\{/)
assert.match(legacyMainCss, /svg\s*\{[^}]*width:\s*1em;[^}]*height:\s*1em;/s)
const topHeaderSearch = headerRender.indexOf('aksrt_navbar_header($ctx);')
const topHeaderSearch = headerRender.indexOf('jio_navbar_header($ctx);')
const topHeaderClose = headerRender.indexOf("echo '</div></div></nav></header>';")
const mobileDrawer = headerRender.indexOf('aksrt_nav_mobile_shell($ctx);')
const mobileDrawer = headerRender.indexOf('jio_nav_mobile_shell($ctx);')
assert.ok(topHeaderSearch > -1 && topHeaderSearch < topHeaderClose)
assert.ok(topHeaderClose < mobileDrawer)
assert.match(legacyMainCss, /\.scroll-down\.nav-fixed \.header\.scroll-down-hide\s*\{[^}]*transform:\s*translate3d\(0, -105%, 0\);/s)
@@ -417,21 +417,21 @@ test('header user-card membership stays on one row with a date-only expiry', ()
// Reference VIP bar: legacy payvip-icon geometry, the untouched level-icon
// contract, and the yellow expiry pill.
assert.match(headerUserbox, /radius4 payvip-icon btn-block text-center/)
assert.match(headerUserbox, /aksrt_get_vip_icon\(\$user_id, 'em12 mr6', false\)/)
assert.match(headerUserbox, /jio_get_vip_icon\(\$user_id, 'em12 mr6', false\)/)
assert.match(headerUserbox, /class="ml10 badg jb-yellow vip-expdate-tag"/)
assert.match(legacyMainCss, /\.sub-user-box \.payvip-icon\s*\{[^}]*padding:\s*8px;/s)
assert.match(legacyMainCss, /\.vip-expdate-tag\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.1em 6px;[^}]*vertical-align:\s*0\.1em;/s)
assert.doesNotMatch(vipModalCss, /\.vip-expdate-tag\s*\{/)
assert.doesNotMatch(headerCss, /aksrt-header-payvip/)
assert.doesNotMatch(headerCss, /jio-header-payvip/)
// The CTA wrapper is the reference padding wrapper, not a bespoke hook.
assert.match(headerUserbox, /'<div class="mt10 em09" style="padding:2px;">'/)
assert.doesNotMatch(headerUserbox, /aksrt-header-payvip/)
assert.doesNotMatch(headerUserbox, /jio-header-payvip/)
// Expiry badge follows zib_get_user_vip_exp_date_text(): an expiry that has
// passed reads 会员已过期, a live one reads the bare date, 永久会员 is reserved
// for an explicit permanent membership, and no expiry data renders no badge.
assert.match(headerUserbox, /\$expires <= time\(\)/)
assert.match(headerUserbox, /empty\(\$state\['is_permanent'\]\)/)
assert.doesNotMatch(headerUserbox, /aksrt_get_vip_info/)
assert.doesNotMatch(headerUserbox, /jio_get_vip_info/)
})
test('save contract rejects invalid input and round-trips canonical nested values', () => {
@@ -5,10 +5,10 @@
namespace {
define('ABSPATH', __DIR__);
$GLOBALS['aksrt_test_options'] = [];
$GLOBALS['aksrt_test_manage_options'] = false;
$GLOBALS['aksrt_visibility_query'] = '';
$GLOBALS['aksrt_visibility_page_config'] = [];
$GLOBALS['jio_test_options'] = [];
$GLOBALS['jio_test_manage_options'] = false;
$GLOBALS['jio_visibility_query'] = '';
$GLOBALS['jio_visibility_page_config'] = [];
final class WP_Query {
/** @var array<string,mixed> */
@@ -62,7 +62,7 @@ namespace {
}
function get_template_directory_uri(): string {
return 'https://example.test/wp-content/themes/aksrt';
return 'https://example.test/wp-content/themes/jio';
}
function home_url($path = ''): string {
@@ -70,27 +70,27 @@ namespace {
}
function get_bloginfo($show = ''): string {
return $show === 'title' ? 'AKSRT' : 'AKSRT';
return $show === 'title' ? 'Jio' : 'Jio';
}
function get_option($key, $default = false) {
return array_key_exists($key, $GLOBALS['aksrt_test_options']) ? $GLOBALS['aksrt_test_options'][$key] : $default;
return array_key_exists($key, $GLOBALS['jio_test_options']) ? $GLOBALS['jio_test_options'][$key] : $default;
}
function get_query_var(string $key) {
if ($key === 'aksrt_message_view' && $GLOBALS['aksrt_visibility_query'] === 'message') {
if ($key === 'jio_message_view' && $GLOBALS['jio_visibility_query'] === 'message') {
return 'unread';
}
return '';
}
function update_option($key, $value): bool {
$GLOBALS['aksrt_test_options'][$key] = $value;
$GLOBALS['jio_test_options'][$key] = $value;
return true;
}
function current_user_can($capability): bool {
return $capability === 'manage_options' && !empty($GLOBALS['aksrt_test_manage_options']);
return $capability === 'manage_options' && !empty($GLOBALS['jio_test_manage_options']);
}
function is_admin(): bool { return false; }
@@ -98,37 +98,37 @@ namespace {
function is_home(): bool {
// Custom rewrites can leave WordPress's posts-page flag set even though
// the theme resolves the request to another front-end surface.
return in_array($GLOBALS['aksrt_visibility_query'], [
return in_array($GLOBALS['jio_visibility_query'], [
'home', 'page', 'message', 'user_home', 'user_center', 'user_home_page', 'user_center_page',
], true);
}
function is_front_page(): bool { return $GLOBALS['aksrt_visibility_query'] === 'home'; }
function is_category(): bool { return $GLOBALS['aksrt_visibility_query'] === 'category'; }
function is_tag(): bool { return $GLOBALS['aksrt_visibility_query'] === 'tag'; }
function is_author(): bool { return $GLOBALS['aksrt_visibility_query'] === 'author'; }
function is_singular(string $type = ''): bool { return $type === 'post' && $GLOBALS['aksrt_visibility_query'] === 'single'; }
function is_search(): bool { return $GLOBALS['aksrt_visibility_query'] === 'search'; }
function is_front_page(): bool { return $GLOBALS['jio_visibility_query'] === 'home'; }
function is_category(): bool { return $GLOBALS['jio_visibility_query'] === 'category'; }
function is_tag(): bool { return $GLOBALS['jio_visibility_query'] === 'tag'; }
function is_author(): bool { return $GLOBALS['jio_visibility_query'] === 'author'; }
function is_singular(string $type = ''): bool { return $type === 'post' && $GLOBALS['jio_visibility_query'] === 'single'; }
function is_search(): bool { return $GLOBALS['jio_visibility_query'] === 'search'; }
function is_page(): bool {
return in_array($GLOBALS['aksrt_visibility_query'], ['page', 'user_home_page', 'user_center_page'], true);
return in_array($GLOBALS['jio_visibility_query'], ['page', 'user_home_page', 'user_center_page'], true);
}
function is_page_template(string $template): bool { return false; }
function get_queried_object_id(): int { return 42; }
/** @return mixed */
function get_post_meta(int $post_id, string $key, bool $single = false) {
return $GLOBALS['aksrt_visibility_page_config'];
return $GLOBALS['jio_visibility_page_config'];
}
function aksrt_is_user_home_page(): bool {
return in_array($GLOBALS['aksrt_visibility_query'], ['user_home', 'user_home_page'], true);
function jio_is_user_home_page(): bool {
return in_array($GLOBALS['jio_visibility_query'], ['user_home', 'user_home_page'], true);
}
function aksrt_is_user_center_page(): bool {
return in_array($GLOBALS['aksrt_visibility_query'], ['user_center', 'user_center_page'], true);
function jio_is_user_center_page(): bool {
return in_array($GLOBALS['jio_visibility_query'], ['user_center', 'user_center_page'], true);
}
}
namespace AKSRT\Settings {
namespace Jio\Settings {
class Settings {
/** @var array<string,mixed> */
public static array $headerSlider = [];
@@ -154,13 +154,13 @@ namespace {
require_once dirname(__DIR__, 2) . '/src/Admin/ControllerBase.php';
require_once dirname(__DIR__, 2) . '/inc/helpers/homepage/header-components.php';
$controller = new class extends \AKSRT\Admin\ControllerBase {
$controller = new class extends \Jio\Admin\ControllerBase {
public function register_routes(): void {}
};
$provider = \AKSRT\Admin\SchemaProvider::get_instance();
$provider = \Jio\Admin\SchemaProvider::get_instance();
$permission_denied = !$controller->check_permission();
$GLOBALS['aksrt_test_manage_options'] = true;
$GLOBALS['jio_test_manage_options'] = true;
$permission_allowed = $controller->check_permission();
$unknown_section = $provider->get_section_schema('page_display', 'missing') === null;
$homepage_defaults = $provider->get_defaults('page_display', 'homepage');
@@ -220,15 +220,15 @@ namespace {
* @param list<string> $show_on
* @param array<string,mixed> $page_config
*/
function aksrt_visibility_case(string $query, array $show_on, array $page_config = [], int $paged = 0): bool {
$GLOBALS['aksrt_visibility_query'] = $query;
$GLOBALS['aksrt_visibility_page_config'] = $page_config;
function jio_visibility_case(string $query, array $show_on, array $page_config = [], int $paged = 0): bool {
$GLOBALS['jio_visibility_query'] = $query;
$GLOBALS['jio_visibility_page_config'] = $page_config;
$GLOBALS['wp_query'] = new WP_Query(['paged' => $paged]);
\AKSRT\Settings\Settings::$headerSlider = [
\Jio\Settings\Settings::$headerSlider = [
'show_on_pages' => $show_on,
'show_type' => '',
];
return aksrt_header_slide_is_show();
return jio_header_slide_is_show();
}
$enabled_page_override = [
@@ -236,22 +236,22 @@ namespace {
'header_slider_show_type' => '',
];
$visibility = [
'disabled' => aksrt_visibility_case('home', []),
'disabled_with_page_override' => aksrt_visibility_case('page', [], $enabled_page_override),
'page_enabled' => aksrt_visibility_case('page', ['page'], $enabled_page_override),
'public_user_home' => aksrt_visibility_case('user_home', ['user_home']),
'user_center' => aksrt_visibility_case('user_center', ['user_center']),
'legacy_author_public_home' => aksrt_visibility_case('user_home', ['author']),
'generic_page_excludes_home' => aksrt_visibility_case('user_home_page', ['page']),
'generic_page_excludes_center' => aksrt_visibility_case('user_center_page', ['page']),
'templated_public_user_home' => aksrt_visibility_case('user_home_page', ['user_home']),
'templated_user_center' => aksrt_visibility_case('user_center_page', ['user_center']),
'home_excludes_page' => aksrt_visibility_case('page', ['home']),
'message_uses_other_page' => aksrt_visibility_case('message', ['page']),
'message_excludes_home' => aksrt_visibility_case('message', ['home']),
'home_excludes_public_home' => aksrt_visibility_case('user_home', ['home']),
'home_excludes_user_center' => aksrt_visibility_case('user_center', ['home']),
'paged_archive_hidden' => aksrt_visibility_case('home', ['home'], [], 2),
'disabled' => jio_visibility_case('home', []),
'disabled_with_page_override' => jio_visibility_case('page', [], $enabled_page_override),
'page_enabled' => jio_visibility_case('page', ['page'], $enabled_page_override),
'public_user_home' => jio_visibility_case('user_home', ['user_home']),
'user_center' => jio_visibility_case('user_center', ['user_center']),
'legacy_author_public_home' => jio_visibility_case('user_home', ['author']),
'generic_page_excludes_home' => jio_visibility_case('user_home_page', ['page']),
'generic_page_excludes_center' => jio_visibility_case('user_center_page', ['page']),
'templated_public_user_home' => jio_visibility_case('user_home_page', ['user_home']),
'templated_user_center' => jio_visibility_case('user_center_page', ['user_center']),
'home_excludes_page' => jio_visibility_case('page', ['home']),
'message_uses_other_page' => jio_visibility_case('message', ['page']),
'message_excludes_home' => jio_visibility_case('message', ['home']),
'home_excludes_public_home' => jio_visibility_case('user_home', ['home']),
'home_excludes_user_center' => jio_visibility_case('user_center', ['home']),
'paged_archive_hidden' => jio_visibility_case('home', ['home'], [], 2),
];
echo json_encode([
+23 -23
View File
@@ -9,10 +9,10 @@ const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
const pay = read('inc/helpers/aksrt-pay.php');
const pay = read('inc/helpers/jio-pay.php');
const widget = read('src/WidgetSystem/Renderers/ContentRenderer.php');
const shell = read('src/WidgetSystem/FrontendRenderer.php');
const single = read('inc/helpers/aksrt-single-render.php');
const single = read('inc/helpers/jio-single-render.php');
const hooks = read('inc/core-hooks.php');
assert.match(pay, /zib-widget pay-box pay-widget/);
@@ -22,8 +22,8 @@ test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
assert.match(pay, /pay-tag abs-center/);
assert.match(pay, /price-box/);
assert.match(pay, /vip-price-buts but-average inline/);
assert.match(pay, /function aksrt_get_show_pay_price_html/);
assert.match(pay, /function aksrt_get_posts_vip_price/);
assert.match(pay, /function jio_get_show_pay_price_html/);
assert.match(pay, /function jio_get_posts_vip_price/);
assert.match(pay, /get_payment_vip_settings/);
assert.match(pay, /\['img_icon'\]/);
assert.match(pay, /signin-loader/);
@@ -35,9 +35,9 @@ test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
assert.doesNotMatch(pay, /\$vip_minor > 0/);
assert.doesNotMatch(pay, /\$vip_items/);
assert.doesNotMatch(pay, /wp_login_url\(get_permalink\(\$post\)\)/);
assert.doesNotMatch(pay, /aksrt_get_user_center_tab_url\('vip'\)/);
assert.doesNotMatch(pay, /jio_get_user_center_tab_url\('vip'\)/);
assert.match(pay, /cashier-link/);
assert.match(pay, /function aksrt_get_paid_box_html/);
assert.match(pay, /function jio_get_paid_box_html/);
assert.match(pay, /pay-box zib-widget paid-box order-type-/);
assert.match(pay, /flex ac jb-green padding-10 em09/);
assert.match(pay, /订单号/);
@@ -47,14 +47,14 @@ test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
assert.match(pay, /hidden-box show/);
assert.match(widget, /'layout' => 'widget'/);
assert.match(shell, /'post_pay', 'oneline_posts', 'hot_posts'/);
assert.doesNotMatch(single, /aksrt-panel pay-box pay-type-download/);
assert.doesNotMatch(single, /jio-panel pay-box pay-type-download/);
assert.doesNotMatch(single, /get_template_part\('templates\/parts\/resource-download-box'\)/);
assert.match(single, /aksrt_get_paid_box_html\(\$post\)/);
assert.doesNotMatch(hooks, /aksrt_paid_content_enhance/);
assert.match(single, /jio_get_paid_box_html\(\$post\)/);
assert.doesNotMatch(hooks, /jio_paid_content_enhance/);
});
test('the purchase card owns one direct stylesheet and no retired paywall path', () => {
const pay = read('inc/helpers/aksrt-pay.php');
const pay = read('inc/helpers/jio-pay.php');
const shortcodes = read('src/Core/Shortcodes.php');
const assets = read('src/Core/ScriptsPart1b.php') + read('src/WidgetSystem/AssetManager.php');
const hidden = read('assets/css/frontend/single-post/hidden-content.css');
@@ -64,7 +64,7 @@ test('the purchase card owns one direct stylesheet and no retired paywall path',
const download = read('templates/parts/resource-download-box.php');
assert.match(assets, /assets\/css\/frontend\/pay-box\.css|\'pay-box\'/);
assert.match(shortcodes, /return aksrt_get_paywall_html\(\$post, \['message' => \$message\]\);/);
assert.match(shortcodes, /return jio_get_paywall_html\(\$post, \['message' => \$message\]\);/);
assert.match(css, /\.zib-widget\.pay-box/);
assert.match(css, /\.pay-box \.pay-vip-level-icon/);
assert.match(css, /\.pay-box \.pay-vip-exclusive/);
@@ -73,12 +73,12 @@ test('the purchase card owns one direct stylesheet and no retired paywall path',
assert.match(publicCss, /\.pay-flexbox\s*\{\s*flex-wrap:\s*wrap;/);
const retired = pay + hidden + widgets + shortcodes;
assert.doesNotMatch(retired, /aksrt-paywall__(?:card|icon|body|title|desc|price|actions|btn|hint|login-link)/);
assert.doesNotMatch(retired, /aksrt-post-pay/);
assert.doesNotMatch(retired, /str_replace\([\s\S]*aksrt-paywall__desc/);
assert.doesNotMatch(pay, /aksrt-paid-(?:gallery|video)__(?:locked|preview|overlay|placeholder|price)/);
assert.doesNotMatch(retired, /jio-paywall__(?:card|icon|body|title|desc|price|actions|btn|hint|login-link)/);
assert.doesNotMatch(retired, /jio-post-pay/);
assert.doesNotMatch(retired, /str_replace\([\s\S]*jio-paywall__desc/);
assert.doesNotMatch(pay, /jio-paid-(?:gallery|video)__(?:locked|preview|overlay|placeholder|price)/);
assert.doesNotMatch(pay + download, /安全检测|支持 VIP 权益|支持积分结算/);
assert.doesNotMatch(download, /function_exists\('aksrt_get_pay_service_html'\)/);
assert.doesNotMatch(download, /function_exists\('jio_get_pay_service_html'\)/);
});
test('the article cashier uses the reference modal contract and one direct asset path', () => {
@@ -92,18 +92,18 @@ test('the article cashier uses the reference modal contract and one direct asset
assert.match(cashier, /muted-box padding-h6 line-16/);
assert.match(cashier, /payment-method-radio hollow-radio flex-auto pointer/);
assert.match(cashier, /mt6 but jb-red initiate-pay btn-block radius/);
assert.match(cashier, /data-aksrt-payment-method/);
assert.match(cashier, /data-jio-payment-method/);
assert.match(cashier, /pay-(?:balance|alipay|wechat|paypal|stripe)-logo\.svg/);
assert.doesNotMatch(cashier, /document\.createElement\(['"]style['"]\)/);
assert.doesNotMatch(cashier, /apiKind|_api_kind|payApiBase/);
assert.doesNotMatch(cashier, /\/(?:shop|forum|bbs)\//i);
assert.match(css, /\.aksrt-dl-modal \.modal-colorful-header/);
assert.match(css, /\.aksrt-dl-modal \.payment-method-radio/);
assert.match(css, /\.aksrt-dl-modal \.but\.initiate-pay/);
assert.match(css, /\.aksrt-dl-modal\.is-cashier \.aksrt-dl-modal__body\s*\{[^}]*padding-top:\s*120px;/s);
assert.match(css, /\.aksrt-dl-modal \.modal-pay-body\s*\{[^}]*margin-top:\s*0;/s);
assert.doesNotMatch(css, /\.aksrt-dl-modal \.modal-pay-body\s*\{[^}]*margin-top:\s*120px;/s);
assert.match(css, /\.jio-dl-modal \.modal-colorful-header/);
assert.match(css, /\.jio-dl-modal \.payment-method-radio/);
assert.match(css, /\.jio-dl-modal \.but\.initiate-pay/);
assert.match(css, /\.jio-dl-modal\.is-cashier \.jio-dl-modal__body\s*\{[^}]*padding-top:\s*120px;/s);
assert.match(css, /\.jio-dl-modal \.modal-pay-body\s*\{[^}]*margin-top:\s*0;/s);
assert.doesNotMatch(css, /\.jio-dl-modal \.modal-pay-body\s*\{[^}]*margin-top:\s*120px;/s);
assert.match(assets, /assets\/css\/frontend\/cms-download\.css/);
assert.match(assets, /filemtime\(\$cms_download_css_path\)/);
@@ -3,7 +3,7 @@
*
* 背景:后台 收款接口 → 收款接口显示顺序 曾整列空白、保存后顺序被静默改写,
* 且没有任何前台代码消费该配置。本契约锁定四件事:
* 1) sortable 项使用 AksField 实际读取的 `label` 键(写 `title` 会渲染成空白);
* 1) sortable 项使用 JioField 实际读取的 `label` 键(写 `title` 会渲染成空白);
* 2) 持久化必须保留用户提交的键顺序(键顺序即配置值本身);
* 3) 排序只保留一份实现,并且真的作用到收银台;
* 4) 相关的重复实现 / 死模块不回潮。
@@ -18,9 +18,9 @@ const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
const schemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'gateway.js');
const aksField = read('assets', 'admin', 'src', 'components', 'form', 'AksField.vue');
const aksField = read('assets', 'admin', 'src', 'components', 'form', 'JioField.vue');
const provider = read('src', 'Admin', 'SchemaProvider.php');
const pay = read('inc', 'helpers', 'aksrt-pay.php');
const pay = read('inc', 'helpers', 'jio-pay.php');
const gateway = read('src', 'Payment', 'Gateway.php');
const wallet = read('inc', 'helpers', 'user-center-renderers', 'credits-wallet.php');
const pointsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
@@ -61,8 +61,8 @@ test('sortable 持久化保留提交键顺序', () => {
});
test('排序只有一份实现,无调用方的重复实现已移除', () => {
assert.match(pay, /if \(!function_exists\('aksrt_sort_payment_methods'\)\)/);
assert.match(pay, /function aksrt_sort_payment_methods\(array \$methods\): array/);
assert.match(pay, /if \(!function_exists\('jio_sort_payment_methods'\)\)/);
assert.match(pay, /function jio_sort_payment_methods\(array \$methods\): array/);
assert.match(pay, /\$configured = is_array\(\$settings\) \? \(\$settings\['pay_sdk_order'\] \?\? \[\]\) : \[\];/);
// 槽位映射与后台 sortable 项一一对应,易支付子通道随所属渠道槽位。
assert.match(pay, /'wechat'\s+=> \['wechatpay', 'epay_wxpay'\]/);
@@ -77,19 +77,19 @@ test('排序只有一份实现,无调用方的重复实现已移除', () => {
test('基础渠道列表与卡密渠道都经过同一排序实现', () => {
// 内容收银台 / 会员购买 / 余额充值共用基础列表。
assert.match(pay, /return aksrt_sort_payment_methods\(\$methods\);/);
assert.match(balanceCharge, /aksrt_sort_payment_methods\(\$methods\)/);
assert.match(vipPackages, /aksrt_sort_payment_methods\(\$methods\)/);
assert.match(pay, /return jio_sort_payment_methods\(\$methods\);/);
assert.match(balanceCharge, /jio_sort_payment_methods\(\$methods\)/);
assert.match(vipPackages, /jio_sort_payment_methods\(\$methods\)/);
// 购买积分弹窗自建余额 + 卡密列表,同样排序。
assert.match(pointsRenderer, /aksrt_sort_payment_methods\(\$points_methods\)/);
assert.match(pointsRenderer, /jio_sort_payment_methods\(\$points_methods\)/);
// 卡密输入盒是 method 条件块,位置无关:卡密渠道可被移到任意槽位。
assert.match(wallet, /in_array\('card_pass', array_column\(\$methods, 'id'\), true\)/);
});
test('会员弹窗不再保留重复的支付方式列表回退分支', () => {
assert.doesNotMatch(vipPackages, /class_exists\('\\\\AKSRT\\\\Payment\\\\Gateway'\)/);
assert.doesNotMatch(vipPackages, /class_exists\('\\\\Jio\\\\Payment\\\\Gateway'\)/);
// 卡密渠道注入契约保持(对齐参考主题 zibpay_get_payment_methods)。
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'aksrt'\)/);
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'jio'\)/);
});
test('未被引用的后台 schema 默认值死模块不回潮', () => {
@@ -48,7 +48,7 @@ test('front-end remove buttons discard the pending upload', () => {
const core = read('assets/js/ticket-center-core.js');
const report = read('assets/js/report.js');
assert.match(core, /'\/upload\/pending\/' \+ id/);
assert.match(core, /data-aksrt-ticket-image-id/);
assert.match(core, /data-jio-ticket-image-id/);
assert.match(report, /'\/upload\/pending\/'/);
});
@@ -65,11 +65,11 @@ test('front submission uploads are marked and saved references are claimed witho
test('cleanup is scheduled once hourly and discard requires a logged-in user', () => {
const state = harness().registration;
assert.ok(state.hooks.includes('aksrt_pending_upload_cleanup'));
assert.ok(state.hooks.includes('jio_pending_upload_cleanup'));
assert.ok(state.hooks.includes('init'));
assert.equal(state.schedule_count, 1);
assert.equal(state.recurrence, 'hourly');
assert.equal(state.namespace, 'aksrt/v1');
assert.equal(state.namespace, 'jio/v1');
assert.equal(state.method, 'DELETE');
assert.equal(state.member_allowed, true);
assert.equal(state.guest_allowed, false);
@@ -95,7 +95,7 @@ test('cleanup deletes only expired unattached pending uploads and uses prepared
assert.match(state.query.sql, /p\.post_type = 'attachment' AND p\.post_parent = 0/);
assert.match(state.query.sql, /m\.meta_key = %s/);
assert.match(state.query.sql, /LIMIT %d/);
assert.equal(state.query.params[0], '_aksrt_pending_upload');
assert.equal(state.query.params[0], '_jio_pending_upload');
assert.equal(state.query.params[2], 100);
assert.ok(Math.abs(state.query.params[1] - (Math.floor(Date.now() / 1000) - 86400)) < 10);
});
Loaded 100 of 997 files, more files were not shown because too many files have changed in this diff. Show more