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

@@ -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);
});
@@ -81,7 +81,7 @@ final class PendingWpdb {
}
require dirname(__DIR__, 2) . '/src/Attachment/PendingUploads.php';
use AKSRT\Attachment\PendingUploads;
use Jio\Attachment\PendingUploads;
function reset_fixture(): void {
$GLOBALS['posts'] = [];
@@ -48,12 +48,12 @@ test('front submit follows the reference newposts shell and cards', () => {
assert.match(page, /<\/div>\s*<\/div>\s*<div class="sidebar show-sidebar">/);
assert.match(layout, /if \(is_page_template\('templates\/pages\/page-submit\.php'\)\) \{\s*return true;/);
assert.equal((page.match(/class="sidebar show-sidebar"/g) || []).length, 1);
assert.match(page, /'aksrt-submit',[\s\S]*?\['aksrt-public'\]/);
assert.match(page, /'jio-submit',[\s\S]*?\['jio-public'\]/);
assert.match(main, /zib-widget full-widget-sm editor-main-box/);
assert.match(main, /relative newposts-title/);
assert.match(terms, /newpost-cat-drop drop-select dropdown dependency-box/);
assert.match(terms, /data-toggle-class="open" data-target="\.newpost-cat-drop"/);
assert.match(terms, /aksrt_submit_render_term_checks\(\$ctx\['categories'\], array_map\('intval', \$ctx\['current_cats'\]\), 'category'\)/);
assert.match(terms, /jio_submit_render_term_checks\(\$ctx\['categories'\], array_map\('intval', \$ctx\['current_cats'\]\), 'category'\)/);
assert.doesNotMatch(terms, /aria-expanded=/);
assert.doesNotMatch(submitJs, /bindDropSelect|term-select-box \[data-toggle-class/);
assert.doesNotMatch(submitCss, /\.term-select-box \.dropdown-menu|\.term-select-box \.drop-select\s*{/);
@@ -66,17 +66,17 @@ test('front submit follows the reference newposts shell and cards', () => {
assert.equal((submit.match(/fa-fw/g) || []).length, 2);
assert.doesNotMatch(submit, /fa-(?:dot-circle|check-square) mr6/);
assert.match(publicCss, /\.but-average\s*>\s*\.but\s*{[^}]*flex:\s*auto/);
assert.match(submitCss, /#aksrt-submit-form \.new-posts-submit\s*{[^}]*white-space:\s*nowrap;/);
assert.match(submitCss, /#aksrt-submit-form \.but-average > \.new-posts-submit\s*{[^}]*min-width:\s*0;[^}]*flex:\s*1 1 50%;/);
assert.match(submitCss, /#jio-submit-form \.new-posts-submit\s*{[^}]*white-space:\s*nowrap;/);
assert.match(submitCss, /#jio-submit-form \.but-average > \.new-posts-submit\s*{[^}]*min-width:\s*0;[^}]*flex:\s*1 1 50%;/);
assert.match(mobileSubmitCss, /body \.newposts-wrap\s*{[^}]*float:\s*none;/);
assert.match(mobileSubmitCss, /body \.sidebar\.show-sidebar\s*{[^}]*display:\s*unset;[^}]*width:\s*100%;[^}]*margin-left:\s*0;/);
assert.doesNotMatch(submitCss, /\.newposts-wrap\s*>\s*\.(?:content-layout|sidebar)/);
assert.doesNotMatch(submitCss, /\.newposts-wrap \.new-posts-submit/);
// The form wraps both float columns; flow-root is its only layout job.
assert.match(submitCss, /#aksrt-submit-form\s*{[^}]*display:\s*flow-root;/);
assert.doesNotMatch(submitCss, /#aksrt-submit-form\s*{[^}]*display:\s*(?:grid|flex);/);
assert.match(submitCss, /#jio-submit-form\s*{[^}]*display:\s*flow-root;/);
assert.doesNotMatch(submitCss, /#jio-submit-form\s*{[^}]*display:\s*(?:grid|flex);/);
// Submit chrome gating lives in footer.php; no dead filter/body-class duplicates.
assert.doesNotMatch(read('src/Core/FrontSubmit.php'), /aksrt_floating_buttons_enabled|aksrt_footer_tabbar_enabled|aksrt-hide-float|aksrt-hide-tabbar|disable_chrome_on_submit/);
assert.doesNotMatch(read('src/Core/FrontSubmit.php'), /jio_floating_buttons_enabled|jio_footer_tabbar_enabled|jio-hide-float|jio-hide-tabbar|disable_chrome_on_submit/);
});
test('front submit paid resources use the reference modal editor and compact list', () => {
@@ -86,8 +86,8 @@ test('front submit paid resources use the reference modal editor and compact lis
const downloads = read('assets/js/front-submit-pay-download.js');
const model = read('src/Core/DomainModel.php');
assert.match(page, /aksrt-front-submit-pay-download/);
assert.match(submit, /aksrtSubmitPayDownloads\.init/);
assert.match(page, /jio-front-submit-pay-download/);
assert.match(submit, /jioSubmitPayDownloads\.init/);
assert.match(pay, /data-pay-download-add/);
assert.match(pay, /data-pay-download-edit/);
assert.match(pay, /data-pay-download-remove/);
@@ -126,7 +126,7 @@ test('the post editor resource row mirrors the reference seven fields with icon
// Accordion + drag reorder affordances.
assert.match(meta, /data-group-toggle/);
assert.match(meta, /data-group-drag/);
assert.match(meta, /aksrt-icon-picker_assets\(|aksrt_icon_picker_field\(/);
assert.match(meta, /jio-icon-picker_assets\(|jio_icon_picker_field\(/);
// The swatch palette and the sanitizer share one class whitelist.
assert.match(meta, /DomainModel::DOWNLOAD_BUTTON_CLASSES/);
assert.match(model, /const DOWNLOAD_BUTTON_CLASSES/);
@@ -139,24 +139,24 @@ test('the post editor resource row mirrors the reference seven fields with icon
// Reindexing rewrites the row index for every field and is scoped to its own
// group, so dragging a download row can never renumber attributes/episodes.
const reindex = (group, name, index) =>
name.replace(new RegExp(`aksrt_pay\\[${group}\\]\\[\\d+\\]`), `aksrt_pay[${group}][${index}]`);
name.replace(new RegExp(`jio_pay\\[${group}\\]\\[\\d+\\]`), `jio_pay[${group}][${index}]`);
['url', 'label', 'extract_code', 'copy_key', 'copy_val', 'icon', 'class'].forEach((field) => {
assert.equal(reindex('downloads', `aksrt_pay[downloads][0][${field}]`, 3), `aksrt_pay[downloads][3][${field}]`);
assert.equal(reindex('downloads', `jio_pay[downloads][0][${field}]`, 3), `jio_pay[downloads][3][${field}]`);
});
assert.equal(reindex('downloads', 'aksrt_pay[downloads][5][url]', 1), 'aksrt_pay[downloads][1][url]');
assert.equal(reindex('downloads', 'aksrt_pay[attributes][0][key]', 9), 'aksrt_pay[attributes][0][key]');
assert.equal(reindex('downloads', 'aksrt_pay[video_episode][1][title]', 9), 'aksrt_pay[video_episode][1][title]');
assert.equal(reindex('downloads', 'jio_pay[downloads][5][url]', 1), 'jio_pay[downloads][1][url]');
assert.equal(reindex('downloads', 'jio_pay[attributes][0][key]', 9), 'jio_pay[attributes][0][key]');
assert.equal(reindex('downloads', 'jio_pay[video_episode][1][title]', 9), 'jio_pay[video_episode][1][title]');
assert.match(setup, /'icon' => \['type' => 'string'\]/);
// The icon picker registers itself as depending on the caller's handle, so the
// pay-meta script must NOT declare the icon-picker handle back: that cycle makes
// WP_Dependencies recurse until memory is exhausted on every post editor load.
const picker = read('inc/helpers/aksrt-icon-picker.php');
assert.match(picker, /array_merge\(\['jquery', 'aksrt-svg-sprite'\], \$deps\)/);
assert.match(meta, /aksrt_icon_picker_assets\(\['aksrt-pay-meta-box'\]\)/);
const payScript = meta.match(/wp_enqueue_script\('aksrt-pay-meta-box',[^\n]*\)/)[0];
assert.doesNotMatch(payScript, /aksrt-icon-picker/);
const picker = read('inc/helpers/jio-icon-picker.php');
assert.match(picker, /array_merge\(\['jquery', 'jio-svg-sprite'\], \$deps\)/);
assert.match(meta, /jio_icon_picker_assets\(\['jio-pay-meta-box'\]\)/);
const payScript = meta.match(/wp_enqueue_script\('jio-pay-meta-box',[^\n]*\)/)[0];
assert.doesNotMatch(payScript, /jio-icon-picker/);
});
test('front submit paid-content prices and switch use the reference alignment rules', () => {
@@ -175,18 +175,18 @@ test('front submit image and visibility actions live in the TinyMCE toolbar', ()
const submit = read('assets/js/front-submit.js');
const editor = read('assets/js/front-submit-editor.js');
assert.match(main, /\$toolbar1\[\]\s*=\s*'aksrt_image'/);
assert.match(main, /\$toolbar1\[\]\s*=\s*'aksrt_hide'/);
assert.match(main, /\$toolbar1\[\]\s*=\s*'jio_image'/);
assert.match(main, /\$toolbar1\[\]\s*=\s*'jio_hide'/);
assert.match(main, /mce_external_plugins/);
assert.match(main, /aksrt_editor_tools/);
assert.match(editor, /PluginManager\.add\('aksrt_editor_tools'/);
assert.match(editor, /addButton\('aksrt_image'/);
assert.match(editor, /addButton\('aksrt_hide'/);
assert.match(main, /jio_editor_tools/);
assert.match(editor, /PluginManager\.add\('jio_editor_tools'/);
assert.match(editor, /addButton\('jio_image'/);
assert.match(editor, /addButton\('jio_hide'/);
assert.match(editor, /type:\s*'menubutton'/);
assert.match(editor, /data-editor-media-tab="upload"/);
assert.match(editor, /data-editor-media-tab="external"/);
assert.match(editor, /class="aksrt-editor-media__dialog" tabindex="-1"/);
assert.match(editor, /querySelector\('\.aksrt-editor-media__dialog'\)/);
assert.match(editor, /class="jio-editor-media__dialog" tabindex="-1"/);
assert.match(editor, /querySelector\('\.jio-editor-media__dialog'\)/);
assert.doesNotMatch(editor, /focusTarget\s*=\s*root\.querySelector\('\[data-editor-media-tab\]\.is-active'\)/);
assert.match(editor, /\(cfg\.uploads \|\| \{\}\)\.image/);
assert.match(page, /'canUploadImage'\s*=>\s*!empty\(\$ctx\['can_upload_img'\]\)/);
@@ -202,12 +202,12 @@ test('front submit exposes the complete reference media and formatting toolset',
const editorCss = read('assets/css/frontend/pages/submit-editor.css');
const submit = read('src/Core/FrontSubmit.php');
['aksrt_video', 'aksrt_file', 'aksrt_quote', 'aksrt_precode'].forEach((button) => {
['jio_video', 'jio_file', 'jio_quote', 'jio_precode'].forEach((button) => {
assert.match(main, new RegExp(`['"]${button}['"]`));
assert.match(editor, new RegExp(`addButton\\(['"]${button}['"]`));
});
assert.doesNotMatch(main, /array_merge\(\$toolbar1, \['blockquote'/);
assert.match(main, /\['aksrt_quote', 'aksrt_precode', 'fullscreen', 'wp_adv'\]/);
assert.match(main, /\['jio_quote', 'jio_precode', 'fullscreen', 'wp_adv'\]/);
assert.match(main, /toolbar2[\s\S]*forecolor/);
assert.match(main, /colorpicker,textcolor/);
@@ -256,13 +256,13 @@ test('article paid-content metabox follows the reference field set without shop
'pay_extra_hide',
];
assert.match(meta, /add_meta_box\('aksrt_pay_meta', __\('付费功能'/);
assert.match(meta, /add_meta_box\('jio_pay_meta', __\('付费功能'/);
fields.forEach((field) => assert.match(meta, new RegExp(field)));
// video_scale_height 是参考主题轮播组件 widget 的参数,不属于付费视频字段,
// 当初误搬到付费 metabox 上且从未被消费,已移除。
assert.doesNotMatch(meta, /video_scale_height/);
assert.doesNotMatch(meta, /aksrt_pay_(?:stock|purchase_limit|card_keys|free_preview|require_login|require_captcha)/);
assert.doesNotMatch(meta, /jio_pay_(?:stock|purchase_limit|card_keys|free_preview|require_login|require_captcha)/);
assert.doesNotMatch(meta, /电商增强|parse_json_array/);
});
@@ -279,5 +279,5 @@ test('ordinary article editor chain does not import shop or forum routes', () =>
const source = files.map(read).join('\n');
assert.doesNotMatch(source, /shop_product|forum_post|bbs_/i);
assert.doesNotMatch(source, /aksrtpay_s|featured_payload|vip_1_(?:price|points)|vip_2_(?:price|points)/);
assert.doesNotMatch(source, /jiopay_s|featured_payload|vip_1_(?:price|points)|vip_2_(?:price|points)/);
});
@@ -9,7 +9,7 @@ const read = file => fs.readFileSync(path.join(root, file), 'utf8')
const list = read('src/Admin/Native/PostListEnhance.php')
const fields = read('src/Admin/Native/PostListEditFields.php')
const domain = read('src/Core/DomainModel.php')
const pay = read('inc/helpers/aksrt-pay.php')
const pay = read('inc/helpers/jio-pay.php')
const metabox = read('src/Core/PostMetaBox.php')
const resourceBox = read('src/Core/ResourceMetaBox.php')
@@ -19,7 +19,7 @@ test('quick and bulk edit boxes hook the post list with no topics column', () =>
assert.match(list, /bulk_edit_custom_box/)
assert.match(list, /COLUMN_STATS/)
// 代码里不得再出现专题分类法列(类文档块的说明除外,只看 namespace 之后的部分)。
const code = list.slice(list.indexOf('namespace AKSRT'))
const code = list.slice(list.indexOf('namespace Jio'))
assert.doesNotMatch(code, /taxonomy-topics/)
assert.doesNotMatch(code, /topics/)
})
@@ -37,9 +37,9 @@ test('every quick-edit field group is rendered', () => {
assert.match(fields, /function pay\(string \$currency\): array/)
})
test('quick-edit inputs are namespaced under aksrt_bulk_edit', () => {
// 字段名统一 aksrt_bulk_edit[{id}][{field}],避免与 WP 原生数据冲突。
assert.match(list, /aksrt_bulk_edit\[' \. \$id \. '\]\[' \. \$field\['id'\] \. '\]/)
test('quick-edit inputs are namespaced under jio_bulk_edit', () => {
// 字段名统一 jio_bulk_edit[{id}][{field}],避免与 WP 原生数据冲突。
assert.match(list, /jio_bulk_edit\[' \. \$id \. '\]\[' \. \$field\['id'\] \. '\]/)
// number 类型带运算子字段。
assert.match(list, /\[operation\]/)
assert.match(list, /\[val\]/)
@@ -63,7 +63,7 @@ test('field-group descriptions are emitted as pre-escaped HTML', () => {
assert.doesNotMatch(list, /esc_html\(\$desc\)/)
// 两个调用点都必须自带转义/HTML 包裹,不能传裸翻译串。
assert.match(list, /'<div class="c-yellow">' \. esc_html__\('批量设置付费参数/)
assert.match(list, /'<div class="px12 aksrt-bulk-edit-desc">' \. esc_html__\('让文章实现更加精细化/)
assert.match(list, /'<div class="px12 jio-bulk-edit-desc">' \. esc_html__\('让文章实现更加精细化/)
// 仍来自设置项的字段标题必须保持转义。
assert.match(list, /esc_html\(\$title_html\)/)
})
@@ -84,7 +84,7 @@ test('interaction quick-edit fields write to the keys the theme reads', () => {
assert.match(fields, /'views'/)
assert.match(fields, /'like'/)
assert.match(metabox, /DomainModel::POST_VIEW_COUNT_KEY, max\(0, \(int\) \(\$raw\['views'\]/)
assert.match(metabox, /'_aksrt_like_count', max\(0, \(int\) \(\$raw\['like'\]/)
assert.match(metabox, /'_jio_like_count', max\(0, \(int\) \(\$raw\['like'\]/)
// 保存侧按这两个键分流到统计 meta。
assert.match(list, /stats_meta_key/)
})
@@ -97,14 +97,14 @@ test('article config quick-edit fields match the extension meta box keys', () =>
// 布局取值白名单:metabox 的 sanitize 是裸数组,快捷编辑提供同四值的单选。
assert.match(metabox, /\['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'\]/)
assert.match(fields, /'false'\s+=> __\('跟随主题', 'aksrt'\)/)
assert.match(fields, /'no_sidebar'\s+=> __\('无侧边栏', 'aksrt'\)/)
assert.match(fields, /'sidebar_left'\s+=> __\('侧边栏靠左', 'aksrt'\)/)
assert.match(fields, /'sidebar_right'\s+=> __\('侧边栏靠右', 'aksrt'\)/)
assert.match(fields, /'false'\s+=> __\('跟随主题', 'jio'\)/)
assert.match(fields, /'no_sidebar'\s+=> __\('无侧边栏', 'jio'\)/)
assert.match(fields, /'sidebar_left'\s+=> __\('侧边栏靠左', 'jio'\)/)
assert.match(fields, /'sidebar_right'\s+=> __\('侧边栏靠右', 'jio'\)/)
// 保存侧再用同一份白名单收敛,避免快捷编辑写入未知布局。
assert.match(list, /\['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'\]/)
// 落到 aksrt_post_meta 这个共享 bag 上,与 metabox 同一键(self::META_KEY)。
// 落到 jio_post_meta 这个共享 bag 上,与 metabox 同一键(self::META_KEY)。
assert.match(metabox, /post_meta/)
assert.match(list, /PostMetaBox::META_KEY/)
})
@@ -133,16 +133,16 @@ test('pay quick-edit save sanitizes through the shared sanitizer', () => {
assert.match(persist, /self::PAY_TYPE_KEY/)
assert.match(persist, /self::PAY_PRICE_KEY/)
assert.match(persist, /self::RESOURCE_MARKER_KEY/)
// _aksrt_pay_modo / _aksrt_pay_limit 没有任何读取方,已删除,不得再写。
assert.doesNotMatch(list, /'_aksrt_pay_modo'|'_aksrt_pay_limit'/)
assert.doesNotMatch(resourceBox, /'_aksrt_pay_modo'|'_aksrt_pay_limit'/)
// _jio_pay_modo / _jio_pay_limit 没有任何读取方,已删除,不得再写。
assert.doesNotMatch(list, /'_jio_pay_modo'|'_jio_pay_limit'/)
assert.doesNotMatch(resourceBox, /'_jio_pay_modo'|'_jio_pay_limit'/)
})
test('pay prices go through the ledger, not raw floats', () => {
// 金额字段运算在「分」上进行,避免浮点误差。
assert.match(list, /balance_minor|price_to_minor/)
assert.match(list, /decimal_from_minor/)
assert.match(list, /\$pay_meta\[\$field_id\] = \\AKSRT\\Ledger\\AssetAmount::decimal_from_minor/)
assert.match(list, /\$pay_meta\[\$field_id\] = \\Jio\\Ledger\\AssetAmount::decimal_from_minor/)
})
test('quick-edit save ignores invalid operations and clamps negatives', () => {
@@ -162,8 +162,8 @@ test('custom filter quick-edit fields reuse the settings dimensions', () => {
// 高级筛选项从同一份设置里动态生成,键名前缀与 metabox 保存一致。
assert.match(list, /get_post_filter_settings/)
assert.match(list, /custom_filter/)
assert.match(list, /'_aksrt_cf_' \. \$field_id/)
assert.match(metabox, /'_aksrt_cf_' \. \$key/)
assert.match(list, /'_jio_cf_' \. \$field_id/)
assert.match(metabox, /'_jio_cf_' \. \$key/)
})
test('quick-edit save is guarded by screen, autosave, revision and capability', () => {
@@ -191,19 +191,19 @@ test('the old inline column code was fully migrated', () => {
assert.doesNotMatch(hooks, /manage_post_posts_columns/)
assert.doesNotMatch(hooks, /manage_post_posts_custom_column/)
assert.doesNotMatch(hooks, /manage_edit-post_sortable_columns/)
assert.doesNotMatch(hooks, /aksrt_enqueue_fontawesome/)
assert.doesNotMatch(hooks, /aksrt_stats/)
assert.doesNotMatch(hooks, /jio_enqueue_fontawesome/)
assert.doesNotMatch(hooks, /jio_stats/)
})
test('the dead tab-pane and modal css blocks are removed', () => {
// 这些选择器无任何引用,已确认删除;文件仍保留 quick-edit 需要的轮子类。
const css = read('assets/css/admin-main.css')
assert.doesNotMatch(css, /aksrt-tab-pane/)
assert.doesNotMatch(css, /\.aksrt-modal\b/)
assert.doesNotMatch(css, /\.aksrt-modal-backdrop\b/)
assert.doesNotMatch(css, /jio-tab-pane/)
assert.doesNotMatch(css, /\.jio-modal\b/)
assert.doesNotMatch(css, /\.jio-modal-backdrop\b/)
// 快捷编辑卡片与依赖样式仍在。
assert.match(css, /\.aksrt-bulk-edit-box/)
assert.match(css, /\.aksrt-bulk-edit-desc/)
assert.match(css, /\.jio-bulk-edit-box/)
assert.match(css, /\.jio-bulk-edit-desc/)
assert.match(css, /\.flex\s*\{/)
})
@@ -211,6 +211,6 @@ test('the quick-edit dependency script exists and toggles by data-dependency', (
assert.ok(fs.existsSync(path.join(root, 'assets/js/admin-post-list.js')))
const js = read('assets/js/admin-post-list.js')
assert.match(js, /data-dependency/)
assert.match(js, /aksrt_bulk_edit\[pay\]/)
assert.match(js, /jio_bulk_edit\[pay\]/)
assert.match(js, /inline-edit-row/)
})
@@ -15,10 +15,10 @@ test('configured thumbnail ratios override body defaults for both list and card
assert.match(main, /^body\s*\{[^}]*--posts-card-scale:\s*70%;[^}]*--posts-list-scale:\s*70%;/s);
for (const [scenario, list, card] of [['defaults', 70, 70], ['custom', 100, 56], ['bounds', 50, 200]]) {
const html = rendered.styles[scenario];
const body = html.match(/<style id="aksrt-theme-color">body\{([^}]+)\}/)[1];
const body = html.match(/<style id="jio-theme-color">body\{([^}]+)\}/)[1];
assert.match(body, new RegExp(`--posts-list-scale:${list}%;`));
assert.match(body, new RegExp(`--posts-card-scale:${card}%;`));
const rootVariables = html.match(/<style id="aksrt-runtime-vars">:root\{([^}]+)\}/)[1];
const rootVariables = html.match(/<style id="jio-runtime-vars">:root\{([^}]+)\}/)[1];
assert.doesNotMatch(rootVariables, /--posts-(list|card)-scale/);
assert.equal((html.match(/--posts-list-scale:/g) || []).length, 1);
assert.equal((html.match(/--posts-card-scale:/g) || []).length, 1);
@@ -2,7 +2,7 @@
/** Render thumbnail settings and image markup without a WordPress installation. */
declare(strict_types=1);
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static array $values = [];
public static function get_all(): array { return self::$values; }
@@ -23,7 +23,7 @@ namespace AKSRT\Settings {
}
}
namespace AKSRT\Core {
namespace Jio\Core {
final class PostMetaBox {
public static array $meta = [];
public static function get_meta_value(int $id, string $key, $default = '') { return self::$meta[$key] ?? $default; }
@@ -53,11 +53,11 @@ namespace {
function get_post_field($field, $id, $context = ''): string { return $GLOBALS['post_content']; }
function get_the_category($id): array { return []; }
function get_post_format($id = null) { return false; }
function aksrt_get_lazy_thumb($size = ''): string { return 'https://example.test/placeholder.svg'; }
function aksrt_is_lazy($key): bool { return $key === 'lazy_sider'; }
function aksrt_get_lazy_attr($key, $src, $class = ''): string {
if (aksrt_is_lazy($key)) {
return ' class="lazyload ' . esc_attr($class) . '" src="' . aksrt_get_lazy_thumb() . '" data-src="' . esc_url($src) . '"';
function jio_get_lazy_thumb($size = ''): string { return 'https://example.test/placeholder.svg'; }
function jio_is_lazy($key): bool { return $key === 'lazy_sider'; }
function jio_get_lazy_attr($key, $src, $class = ''): string {
if (jio_is_lazy($key)) {
return ' class="lazyload ' . esc_attr($class) . '" src="' . jio_get_lazy_thumb() . '" data-src="' . esc_url($src) . '"';
}
return ' class="' . esc_attr($class) . '" src="' . esc_url($src) . '"';
}
@@ -69,41 +69,41 @@ namespace {
$styles = [];
foreach (['defaults' => [], 'custom' => ['list_list_option' => ['scale' => 100], 'list_card_option' => ['scale' => 56]], 'bounds' => ['list_list_option' => ['scale' => 1], 'list_card_option' => ['scale' => 900]]] as $name => $values) {
\AKSRT\Settings\Settings::$values = $values;
\Jio\Settings\Settings::$values = $values;
ob_start();
(new \AKSRT\Frontend\InlineStyleBuilder())->output();
(new \Jio\Frontend\InlineStyleBuilder())->output();
$styles[$name] = (string) ob_get_clean();
}
$GLOBALS['featured_id'] = 42;
$GLOBALS['post_content'] = '<p><img src="https://example.test/content.jpg"></p>';
\AKSRT\Core\PostMetaBox::$meta = ['thumbnail_url' => 'https://example.test/external.jpg'];
\Jio\Core\PostMetaBox::$meta = ['thumbnail_url' => 'https://example.test/external.jpg'];
$cfg = ['thumb' => ['thumb_postfirstimg_size' => 'large', 'thumb_postfirstimg_s' => true, 'spare_thumbnail' => ['https://example.test/spare.jpg']]];
$featured = aksrt_renderer_resolve_images(1, $cfg);
$featured = jio_renderer_resolve_images(1, $cfg);
$GLOBALS['featured_id'] = 999;
$external = aksrt_renderer_resolve_images(1, $cfg);
\AKSRT\Core\PostMetaBox::$meta = [];
$content = aksrt_renderer_resolve_images(1, $cfg);
$external = jio_renderer_resolve_images(1, $cfg);
\Jio\Core\PostMetaBox::$meta = [];
$content = jio_renderer_resolve_images(1, $cfg);
$GLOBALS['post_content'] = '';
$spare = aksrt_renderer_resolve_images(1, $cfg);
\AKSRT\Core\PostMetaBox::$meta = ['featured_slide' => '999,42'];
$slides = aksrt_renderer_curated_images(1, 'medium');
$spare = jio_renderer_resolve_images(1, $cfg);
\Jio\Core\PostMetaBox::$meta = ['featured_slide' => '999,42'];
$slides = jio_renderer_curated_images(1, 'medium');
$data = [
'post_id' => 1, 'primary' => $featured['primary'], 'has_real' => true,
'permalink' => 'https://example.test/post/', 'title' => 'Featured image',
'video_url' => '', 'slides' => [], 'gallery_count' => 0,
];
$cover = aksrt_renderer_thumb($data, ['thumb' => ['list_thumb_fit' => '']]);
$contain = aksrt_renderer_thumb($data, ['thumb' => ['list_thumb_fit' => 'contain']]);
$cover = jio_renderer_thumb($data, ['thumb' => ['list_thumb_fit' => '']]);
$contain = jio_renderer_thumb($data, ['thumb' => ['list_thumb_fit' => 'contain']]);
$data['is_slide'] = true;
$data['slides'] = ['https://example.test/slide.jpg'];
$single = aksrt_renderer_thumb($data, ['thumb' => []]);
\AKSRT\Core\PostMetaBox::$meta = ['featured_slide' => implode(',', array_fill(0, 12, '42'))];
$data['slides'] = aksrt_renderer_curated_images(1, 'large');
$multiple = aksrt_renderer_thumb($data, ['thumb' => ['list_thumb_fit' => 'contain']]);
$single = jio_renderer_thumb($data, ['thumb' => []]);
\Jio\Core\PostMetaBox::$meta = ['featured_slide' => implode(',', array_fill(0, 12, '42'))];
$data['slides'] = jio_renderer_curated_images(1, 'large');
$multiple = jio_renderer_thumb($data, ['thumb' => ['list_thumb_fit' => 'contain']]);
$data['video_url'] = 'https://example.test/movie.mp4';
$video = aksrt_renderer_thumb($data, ['thumb' => ['list_thumb_video_s' => true, 'list_thumb_video_mute_s' => true]]);
$video = jio_renderer_thumb($data, ['thumb' => ['list_thumb_video_s' => true, 'list_thumb_video_mute_s' => true]]);
echo json_encode(compact('styles', 'featured', 'external', 'content', 'spare', 'slides', 'cover', 'contain', 'single', 'multiple', 'video'), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
}
+178 -178
View File
@@ -19,8 +19,8 @@ const articleFiles = [
'inc/helpers/posts-renderer/config.php',
'inc/helpers/posts-renderer/post-data.php',
'inc/helpers/posts-renderer/markup.php',
'inc/helpers/aksrt-archive.php',
'inc/helpers/aksrt-single-render.php',
'inc/helpers/jio-archive.php',
'inc/helpers/jio-single-render.php',
];
test('the article stream exposes only the new typed renderer contract', () => {
@@ -40,14 +40,14 @@ test('the article stream exposes only the new typed renderer contract', () => {
'templates/parts/user-home/panel-favorites.php',
]);
assert.match(markup, /function aksrt_render_post_list\(array \$args = \[\], \?WP_Query \$query = null, bool \$echo = true\): string/);
assert.doesNotMatch(consumers, /function_exists\(['"]aksrt_render_post_list/);
assert.doesNotMatch(consumers, /aksrt_render_post_list\(\s*\$query\s*,/);
assert.doesNotMatch(consumers, /content-post-card|aksrt_homepage_render_post_card/);
assert.match(markup, /function jio_render_post_list\(array \$args = \[\], \?WP_Query \$query = null, bool \$echo = true\): string/);
assert.doesNotMatch(consumers, /function_exists\(['"]jio_render_post_list/);
assert.doesNotMatch(consumers, /jio_render_post_list\(\s*\$query\s*,/);
assert.doesNotMatch(consumers, /content-post-card|jio_homepage_render_post_card/);
// 搜索页的加载态必须是文章同构骨架,而不是遗留的转圈 spinner,
// 这样 runtime main.js 的 list 型 loader 形状才与搜索结果一致。
assert.match(read('search.php'), /post_ajax_loader search-loader"><\?php echo function_exists\('aksrt_render_placeholder'\) \? aksrt_render_placeholder\('list', 6\)/);
assert.match(read('search.php'), /post_ajax_loader search-loader"><\?php echo function_exists\('jio_render_placeholder'\) \? jio_render_placeholder\('list', 6\)/);
assert.doesNotMatch(read('search.php'), /class="loading mr10"/);
});
@@ -103,8 +103,8 @@ test('forced mode wins and thumb_if_has ignores spare and lazy placeholders', ()
assert.match(config, /\$is_card = \$forced_type === 'card' \|\| \(!\$is_forced/);
const hasReal = config.indexOf("$has_real = $primary !== '';");
const spareCall = config.lastIndexOf('aksrt_renderer_spare_image($thumb)');
const lazy = config.lastIndexOf('aksrt_get_lazy_thumb');
const spareCall = config.lastIndexOf('jio_renderer_spare_image($thumb)');
const lazy = config.lastIndexOf('jio_get_lazy_thumb');
assert.ok(hasReal >= 0 && hasReal < spareCall && spareCall < lazy);
assert.match(data, /'thumb_if_has' && empty\(\$images\['has_real'\]\)/);
assert.match(data, /count\(\$gallery\) > 2/);
@@ -112,7 +112,7 @@ test('forced mode wins and thumb_if_has ignores spare and lazy placeholders', ()
});
test('category card mode carries its term id through archive and shared AJAX rendering', () => {
const resolver = read('inc/helpers/aksrt-archive.php');
const resolver = read('inc/helpers/jio-archive.php');
const config = read('inc/helpers/posts-renderer/config.php');
const admin = read('src/Admin/Admin.php');
const global = read('src/Admin/GlobalController.php');
@@ -125,9 +125,9 @@ test('category card mode carries its term id through archive and shared AJAX ren
assert.doesNotMatch(config, /get_queried_object_id/);
assert.match(admin, /\$clean\['term_id'\]\s*=\s*absint\(\$config\['term_id'\]\)/);
assert.doesNotMatch(global, /rest_archive_posts|\/archive\/posts/);
assert.match(read('category.php'), /aksrt_archive_get_render_config\(\['context' => 'category'\]\)/);
assert.match(read('category.php'), /jio_archive_get_render_config\(\['context' => 'category'\]\)/);
assert.doesNotMatch(config, /\b(?:list_scale|card_scale|mobile_meta)\b/);
assert.doesNotMatch(resolver, /aksrt_archive_get_list_style_settings/);
assert.doesNotMatch(resolver, /jio_archive_get_list_style_settings/);
});
test('image resolution counts only content images and applies the configured WordPress size', () => {
@@ -136,13 +136,13 @@ test('image resolution counts only content images and applies the configured Wor
const thumbSchema = read('assets/admin/src/schema/sections/post-list/thumb.js');
const phpSchema = read('src/Admin/Schema/SchemaPostList.php');
assert.match(config, /function aksrt_renderer_content_images\(int \$post_id, string \$size\)/);
assert.match(config, /function jio_renderer_content_images\(int \$post_id, string \$size\)/);
assert.match(config, /strip_shortcodes\(\(string\) get_post_field\('post_content'/);
assert.match(config, /\$slides = aksrt_renderer_curated_images\(\$post_id, \$size\);\s*\$gallery = aksrt_renderer_content_images\(\$post_id, \$size\);/);
assert.match(config, /get_post_thumbnail_id\(\$post_id\).*aksrt_renderer_sized_image\(\(string\) \$thumbnail_id, \$size\)/s);
assert.match(config, /aksrt_renderer_category_image\(\$post_id, \$size\)/);
assert.doesNotMatch(config, /aksrt_get_post_gallery_images|cover_images|featured_video_episode|_aksrt_featured_video|_video_url|_aksrt_thumbnail_url|_thumbnail_url/);
assert.doesNotMatch(config, /\['aksrt_category_cover_id',/);
assert.match(config, /\$slides = jio_renderer_curated_images\(\$post_id, \$size\);\s*\$gallery = jio_renderer_content_images\(\$post_id, \$size\);/);
assert.match(config, /get_post_thumbnail_id\(\$post_id\).*jio_renderer_sized_image\(\(string\) \$thumbnail_id, \$size\)/s);
assert.match(config, /jio_renderer_category_image\(\$post_id, \$size\)/);
assert.doesNotMatch(config, /jio_get_post_gallery_images|cover_images|featured_video_episode|_jio_featured_video|_video_url|_jio_thumbnail_url|_thumbnail_url/);
assert.doesNotMatch(config, /\['jio_category_cover_id',/);
assert.match(data, /'gallery_count'\s*=>\s*count\(\$content_images\)/);
assert.match(data, /\(int\) \$data\['gallery_count'\] - 4/);
assert.match(thumbSchema, /spareThumbnailDefault = \[\{ img: `\$\{themeRoot\}\/assets\/img\/thumbnail\.svg` \}\]/);
@@ -152,8 +152,8 @@ test('image resolution counts only content images and applies the configured Wor
test('list, card, media, badge and Mini DOM follow dedicated legacy structures', () => {
const markup = read('inc/helpers/posts-renderer/markup.php');
const data = read('inc/helpers/posts-renderer/post-data.php');
const card = markup.slice(markup.indexOf('function aksrt_renderer_card'), markup.indexOf('function aksrt_renderer_mini'));
const mini = markup.slice(markup.indexOf('function aksrt_renderer_mini'), markup.indexOf('function aksrt_render_post'));
const card = markup.slice(markup.indexOf('function jio_renderer_card'), markup.indexOf('function jio_renderer_mini'));
const mini = markup.slice(markup.indexOf('function jio_renderer_mini'), markup.indexOf('function jio_render_post'));
assert.match(markup, /\['posts-item', 'list', 'ajax-item'\]/);
assert.match(markup, /\$classes\[\] = .*'flex xx'.*'flex'/s);
@@ -162,7 +162,7 @@ test('list, card, media, badge and Mini DOM follow dedicated legacy structures',
assert.match(markup, /<div class="item-body flex xx flex1 jsb/);
assert.doesNotMatch(card, /item-excerpt|renderer_excerpt_markup|thumb-items/);
assert.match(data, /<div class="item-thumbnail/);
assert.match(data, /relative aksrt-slider.*new-swiper item-thumbnail/s);
assert.match(data, /relative jio-slider.*new-swiper item-thumbnail/s);
assert.match(data, /swiper-slide.*gradient-bg.*data-opacity="0\.2"/s);
assert.match(data, /\$fit = \$is_contain && !\$has_slides \? ' contain gradient-bg' : ''/);
assert.match(data, /video-thumb-box.*video-url=/s);
@@ -176,7 +176,7 @@ test('list, card, media, badge and Mini DOM follow dedicated legacy structures',
assert.match(mini, /<div class="mr10"><div class="item-thumbnail"><a/);
assert.match(mini, /posts-mini-con flex xx flex1 jsb/);
assert.match(mini, /\['c-red', 'c-yellow', 'c-purple', 'c-blue', 'c-green'\]/);
assert.match(mini, /aksrt_renderer_pay_badge\(\$data, true\)/);
assert.match(mini, /jio_renderer_pay_badge\(\$data, true\)/);
assert.match(data, /meta-pay badg badg-sm mr6/);
assert.doesNotMatch(mini, /posts-mini-row/);
@@ -186,14 +186,14 @@ test('list, card, media, badge and Mini DOM follow dedicated legacy structures',
assert.match(data, /usort\(\$tags,.*->count.*->count/s);
assert.match(data, /查看此标签更多文章/);
const commentMeta = data.indexOf("aksrt_renderer_svg_icon('comment')");
const viewMeta = data.indexOf("aksrt_renderer_svg_icon('view')", commentMeta);
const likeMeta = data.indexOf("aksrt_renderer_svg_icon('like')", viewMeta);
const commentMeta = data.indexOf("jio_renderer_svg_icon('comment')");
const viewMeta = data.indexOf("jio_renderer_svg_icon('view')", commentMeta);
const likeMeta = data.indexOf("jio_renderer_svg_icon('like')", viewMeta);
assert.ok(commentMeta >= 0 && commentMeta < viewMeta && viewMeta < likeMeta);
assert.match(data, /comments_open.*close_comments/s);
assert.match(data, /show_like.*post_type.*=== 'post'/s);
// The <use> sprite markup lives in the unified helper aksrt_renderer_svg_icon delegates to.
assert.match(read('inc/helpers/aksrt-icons.php'), /<use xlink:href="#icon-/);
// The <use> sprite markup lives in the unified helper jio_renderer_svg_icon delegates to.
assert.match(read('inc/helpers/jio-icons.php'), /<use xlink:href="#icon-/);
});
test('post-card CSS owns the exact desktop and 767/640 responsive matrix', () => {
@@ -209,7 +209,7 @@ test('post-card CSS owns the exact desktop and 767/640 responsive matrix', () =>
assert.doesNotMatch(mobile767, /\.posts-mini\s*\{[^}]*width:\s*100%/s);
assert.match(mobile640, /\.posts-mini\s*\{[^}]*width:\s*100%/s);
assert.match(mobile640, /\.posts-item:not\(\.no-thumb\) \.meta-right item\s*\{[^}]*display:\s*none/s);
assert.doesNotMatch(css, /\.aksrt-(?!panel|slider)|post-card-cms|grid-cms/);
assert.doesNotMatch(css, /\.jio-(?!panel|slider)|post-card-cms|grid-cms/);
});
test('article metadata owns its flex and SVG contracts on every list route', () => {
@@ -217,7 +217,7 @@ test('article metadata owns its flex and SVG contracts on every list route', ()
const scripts = read('src/Core/ScriptsPart1c.php');
const loader = read('assets/js/runtime/loader.js');
assert.doesNotMatch(scripts, /aksrt-svg-icon|svg-icon-helper/);
assert.doesNotMatch(scripts, /jio-svg-icon|svg-icon-helper/);
assert.match(loader, /'svg-icon'\s*:\s*'svg-icon'/);
assert.match(css, /\.posts-item\.flex,[\s\S]*?\.posts-mini \.flex\s*\{[^}]*display:\s*flex;/);
assert.match(css, /\.posts-item \.flex\.xx,[\s\S]*?\.posts-mini \.flex\.xx\s*\{[^}]*flex-direction:\s*column;/);
@@ -228,8 +228,8 @@ test('article metadata owns its flex and SVG contracts on every list route', ()
});
test('related posts, author page and widgets use their dedicated renderers', () => {
const related = read('inc/helpers/aksrt-single-render.php');
const relatedCard = read('inc/helpers/aksrt-single-components.php');
const related = read('inc/helpers/jio-single-render.php');
const relatedCard = read('inc/helpers/jio-single-components.php');
const resourceRelated = read('templates/parts/resource-related.php');
const author = read('templates/parts/user-home/panel-posts.php');
const authorFavorites = read('templates/parts/user-home/panel-favorites.php');
@@ -237,40 +237,40 @@ test('related posts, author page and widgets use their dedicated renderers', ()
const widgets = read('src/WidgetSystem/PostRenderer.php');
assert.match(related, /\['img', 'list', 'text'\]/);
assert.match(related, /aksrt_renderer_mini\(\$data, \$mini_cfg\)/);
assert.match(related, /\<div class="swiper-slide mr10"\>' \. aksrt_render_graphic_card_post\(\$post\)/);
assert.match(related, /jio_renderer_mini\(\$data, \$mini_cfg\)/);
assert.match(related, /\<div class="swiper-slide mr10"\>' \. jio_render_graphic_card_post\(\$post\)/);
assert.match(related, /<li><a class="icon-circle"/);
assert.match(related, /\['category', 'topics', 'post_tag'\]/);
assert.match(related, /'terms'\s+=>\s+array_map\('intval', \(array\) \$term_ids\)/);
assert.match(related, /class=\"zib-widget\"/);
assert.doesNotMatch(related, /post_related_source/);
assert.doesNotMatch(related, /post__not_in/);
assert.match(related, /\$args\['meta_key'\]\s*=\s*'aksrt_post_views'/);
assert.match(relatedCard, /aksrt_renderer_resolve_images/);
assert.match(relatedCard, /'text2'\s+=>\s+esc_html\(aksrt_str_cut\(\$title, 0, \(int\) \$args\['cut'\], '\.\.\.'\)\)/);
assert.match(related, /\$args\['meta_key'\]\s*=\s*'jio_post_views'/);
assert.match(relatedCard, /jio_renderer_resolve_images/);
assert.match(relatedCard, /'text2'\s+=>\s+esc_html\(jio_str_cut\(\$title, 0, \(int\) \$args\['cut'\], '\.\.\.'\)\)/);
assert.match(resourceRelated, /swiper-container swiper-scroll/);
assert.match(resourceRelated, /aksrt_render_graphic_card_post/);
assert.doesNotMatch(resourceRelated, /aksrt_render_post_list/);
assert.match(author, /aksrt_render_post_list/);
assert.match(resourceRelated, /jio_render_graphic_card_post/);
assert.doesNotMatch(resourceRelated, /jio_render_post_list/);
assert.match(author, /jio_render_post_list/);
assert.doesNotMatch(author, /'type'\s*=>\s*'list'/);
assert.match(author, /'context'\s*=>\s*'author'/);
assert.match(author, /'no_author'\s*=>\s*true/);
assert.match(author, /'no_margin'\s*=>\s*true/);
assert.match(author, /'wrapper_class'\s*=>\s*'aksrt-user-home__post-list'/);
assert.match(author, /aksrt_render_user_home_pagination\(\(int\) \$author_posts->max_num_pages, \(int\) \$posts_page\)/);
assert.doesNotMatch(author, /aksrt_render_pagination|aksrt_render_profile_pagination|\$_GET|add_query_arg/);
assert.match(author, /'wrapper_class'\s*=>\s*'jio-user-home__post-list'/);
assert.match(author, /jio_render_user_home_pagination\(\(int\) \$author_posts->max_num_pages, \(int\) \$posts_page\)/);
assert.doesNotMatch(author, /jio_render_pagination|jio_render_profile_pagination|\$_GET|add_query_arg/);
// 面板 AJAX 翻页的骨架:与文章列表同一个 .post_ajax_loader 契约,
// 由 author.js 的 .is-loading 切换(不能再靠 opacity 变暗反馈)。
// 骨架形状必须跟随同一份渲染配置:写死 'list' 会让卡片模式下的占位
// 与真实卡片不同轴。
assert.match(author, /'append_inner_html' => \$user_home_panel_loader/);
assert.match(author, /aksrt_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
assert.doesNotMatch(author, /aksrt_render_placeholder\('(list|card)'/);
assert.match(author, /jio_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
assert.doesNotMatch(author, /jio_render_placeholder\('(list|card)'/);
assert.match(authorFavorites, /'append_inner_html' => \$user_home_panel_loader/);
assert.match(authorFavorites, /aksrt_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
assert.doesNotMatch(authorFavorites, /aksrt_render_placeholder\('(list|card)'/);
assert.match(userHomeCss, /\.aksrt-user-home \.dashboard-panel \.post_ajax_loader \{/);
assert.match(userHomeCss, /\.aksrt-user-home \.dashboard-panel\.is-loading \.post_ajax_loader \{/);
assert.match(authorFavorites, /jio_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
assert.doesNotMatch(authorFavorites, /jio_render_placeholder\('(list|card)'/);
assert.match(userHomeCss, /\.jio-user-home \.dashboard-panel \.post_ajax_loader \{/);
assert.match(userHomeCss, /\.jio-user-home \.dashboard-panel\.is-loading \.post_ajax_loader \{/);
assert.doesNotMatch(userHomeCss, /\.dashboard-panel\.is-loading \{[\s\S]{0,80}opacity: \.6/);
// 翻页 / 切 tab 是整页替换语义(author.js 最终就是 panel.innerHTML = html),
@@ -279,29 +279,29 @@ test('related posts, author page and widgets use their dedicated renderers', ()
// beforeSend:replace 时 $item.remove() + find(pag).remove()。
// 刻意不按 data-view 收窄:card 模式下真实卡片同样是直接子级 .posts-item,
// 也要一起收起,否则卡片面板旧内容不会让位。
assert.match(userHomeCss, /\.dashboard-panel\.is-loading \.aksrt-user-home__post-list > \.posts-item \{\s*display: none;/);
assert.match(userHomeCss, /\.dashboard-panel\.is-loading \.jio-user-home__post-list > \.posts-item \{\s*display: none;/);
// 必须收「直接子级」而非后代:骨架内层同样带 .posts-item,
// 写成后代选择器会连骨架自己一起隐藏掉,占位也就永远出不来了。
assert.doesNotMatch(userHomeCss, /\.dashboard-panel\.is-loading \.aksrt-user-home__post-list \.posts-item \{/);
assert.doesNotMatch(userHomeCss, /\.dashboard-panel\.is-loading \.jio-user-home__post-list \.posts-item \{/);
// 旧翻页控件高亮的是旧页码,同属被替换的旧页内容。
assert.match(userHomeCss, /\.dashboard-panel\.is-loading:has\(\.post_ajax_loader\) \.profile-pagination \{\s*display: none;/);
assert.match(widgets, /aksrt_render_mini_list/);
assert.match(widgets, /jio_render_mini_list/);
assert.match(widgets, /swiper-container swiper-scroll/);
});
test('after-box author card mirrors legacy user-card structure', () => {
const render = read('inc/helpers/aksrt-single-render.php');
const identity = read('inc/helpers/aksrt-identity.php');
const avatar = read('inc/helpers/aksrt-avatar.php');
const userCard = read('inc/helpers/aksrt-user-card.php');
const render = read('inc/helpers/jio-single-render.php');
const identity = read('inc/helpers/jio-identity.php');
const avatar = read('inc/helpers/jio-avatar.php');
const userCard = read('inc/helpers/jio-user-card.php');
const css = read('assets/css/frontend/single-post/related-toc.css');
const authorActions = read('assets/css/frontend/single-post/author-actions.css');
const base = read('assets/css/frontend/base.css');
const follow = read('assets/js/follow.js');
const authorBar = read('templates/parts/single-author-bar.php');
const single = read('inc/helpers/aksrt-single.php');
const single = read('inc/helpers/jio-single.php');
const settings = read('src/Settings/PostDisplay.php');
const frontendTags = read('inc/helpers/aksrt-frontend-tags.php');
const frontendTags = read('inc/helpers/jio-frontend-tags.php');
assert.match(render, /avatar-img avatar-lg/);
assert.match(render, /'vip'\s+=>\s+true/);
@@ -320,21 +320,21 @@ test('after-box author card mirrors legacy user-card structure', () => {
assert.match(avatar, /class="avatar-badge"/);
assert.match(userCard, /swiper-container more-posts swiper-scroll/);
assert.match(userCard, /'class'\s+=>\s+'em09', 'cut'\s+=>\s+32/);
assert.doesNotMatch(userCard, /aksrt-more-post-card/);
assert.doesNotMatch(userCard, /jio-more-post-card/);
assert.match(css, /^\.zib-widget \{/m);
assert.doesNotMatch(css, /relates-thumb \.graphic\.style-3 \.graphic-img/);
assert.doesNotMatch(authorActions, /\.user-card-author|\.user-card \.author|user-card-posts/);
assert.match(identity, /function aksrt_get_user_follow\(/);
assert.match(identity, /data-aksrt-follow-label/);
assert.match(identity, /aksrt_get_user_follow\('focus-color ml10 follow flex0', \$user_id\)/);
assert.match(authorBar, /aksrt_get_user_follow\('px12-sm ml10 follow but c-red', \$author_id\)/);
assert.match(identity, /function jio_get_user_follow\(/);
assert.match(identity, /data-jio-follow-label/);
assert.match(identity, /jio_get_user_follow\('focus-color ml10 follow flex0', \$user_id\)/);
assert.match(authorBar, /jio_get_user_follow\('px12-sm ml10 follow but c-red', \$author_id\)/);
assert.doesNotMatch(authorBar, /Follow::get_instance|is_following|show_actions|show_bio|extra_class|meta_items/);
assert.doesNotMatch(single, /aksrt_single_render_author_bar/);
assert.doesNotMatch(single, /jio_single_render_author_bar/);
assert.match(base, /\.user-card\.author \.user-name > name\s*\{[^}]*overflow:\s*visible;/s);
assert.match(base, /\.user-card\.author \.user-name > name > \.follow\s*\{[^}]*flex-shrink:\s*0;/s);
assert.match(follow, /querySelectorAll\('\[data-aksrt-follow="' \+ userId/);
assert.match(follow, /querySelectorAll\('\[data-jio-follow="' \+ userId/);
assert.match(follow, /matchingButtons\.forEach/);
assert.match(follow, /\[data-aksrt-follow-label\]/);
assert.match(follow, /\[data-jio-follow-label\]/);
assert.doesNotMatch(follow, /childNodes|textNode|Fallback when markup/);
assert.doesNotMatch(settings, /'authordesc_s'\s*=>/);
assert.doesNotMatch(frontendTags, /array_key_exists\('authordesc_s'/);
@@ -371,23 +371,23 @@ test('AJAX pagination can recover, caps auto loading and rehydrates appended med
assert.match(js, /post_ajax\.ed/);
assert.match(js, /window\.auto_fun\(\)/);
assert.match(js, /classList\.add\("lazyload"\)/);
assert.match(homepage, /aksrt_render_pagination\(\$query,[\s\S]*?'query_context'\s*=>\s*\$contract\['query_context'\]/);
assert.doesNotMatch(homepage, /href="javascript:;" class="ajax-next aksrt-load-more-btn"/);
assert.match(ajaxList, /if \(link\.closest\("\[data-aksrt-pagination\], \.aksrt-comment-pagination"\)\) return;/);
assert.match(homepage, /jio_render_pagination\(\$query,[\s\S]*?'query_context'\s*=>\s*\$contract\['query_context'\]/);
assert.doesNotMatch(homepage, /href="javascript:;" class="ajax-next jio-load-more-btn"/);
assert.match(ajaxList, /if \(link\.closest\("\[data-jio-pagination\], \.jio-comment-pagination"\)\) return;/);
assert.match(read('assets/js/runtime/main.js'), /thumb_dplayer\(_wid > 640/);
assert.doesNotMatch(media, /new Swiper|data-post-thumb-slider/);
assert.match(read('assets/js/runtime/main.min.js'), /new-swiper/);
assert.doesNotMatch(media, /data-post-thumb-video|initPostThumbMedia/);
assert.match(media, /window\.Grade\(container/);
assert.match(media, /linear-gradient\(135deg/);
assert.match(media, /aksrtGradientState/);
assert.match(media, /jioGradientState/);
assert.match(read('assets/js/runtime/main.js'), /tbquire\(\['grade'\]/);
});
test('front-end carousel CSS is conditional on carousel block content', () => {
const hooks = read('inc/core-hooks.php');
assert.match(hooks, /has_block\('aksrt\/carousel', \$post\)/);
assert.match(hooks, /if \(!\$has_carousel\) return;[\s\S]*?'aksrt-gutenberg-blocks-front'/);
assert.match(hooks, /has_block\('jio\/carousel', \$post\)/);
assert.match(hooks, /if \(!\$has_carousel\) return;[\s\S]*?'jio-gutenberg-blocks-front'/);
});
test('the pinned visual baseline records all source checksums', () => {
@@ -403,39 +403,39 @@ test('the pinned visual baseline records all source checksums', () => {
test('every scene reserves a sidebar column only when its widget stack has content', () => {
const layout = read('inc/helpers/template-tags/site-layout.php');
const single = read('inc/helpers/aksrt-single.php');
const aside = read('inc/helpers/aksrt-single-aside.php');
const single = read('inc/helpers/jio-single.php');
const aside = read('inc/helpers/jio-single-aside.php');
const homepage = read('inc/helpers/homepage/content.php');
const sidebarTemplate = read('sidebar.php');
const archiveHelpers = read('inc/helpers/aksrt-archive.php');
const archiveHelpers = read('inc/helpers/jio-archive.php');
// Single visibility decision: an empty widget stack collapses every scene
// (home/category/tag/search/page/archive) to no-sidebar, not just singles.
assert.match(layout, /if \(!aksrt_has_current_sidebar_widgets\(\)\) \{\s*return false;/);
assert.doesNotMatch(layout, /is_single\(\) && !aksrt_has_current_sidebar_widgets/);
assert.match(layout, /if \(!jio_has_current_sidebar_widgets\(\)\) \{\s*return false;/);
assert.doesNotMatch(layout, /is_single\(\) && !jio_has_current_sidebar_widgets/);
// The resolver must answer home_sidebar before the is_page() branch so a
// static front page is checked against the zones the homepage renders.
const resolver = layout.match(/function aksrt_resolve_sidebar_id\(\): string \{[\s\S]*?\n\}/);
assert.ok(resolver, 'aksrt_resolve_sidebar_id should exist');
const resolver = layout.match(/function jio_resolve_sidebar_id\(\): string \{[\s\S]*?\n\}/);
assert.ok(resolver, 'jio_resolve_sidebar_id should exist');
assert.ok(
resolver[0].indexOf('is_home() || is_front_page()') < resolver[0].indexOf('is_page()'),
'home/front_page branch must precede the is_page() branch'
);
assert.match(single, /\$show_aside = get_post_type\(\$post_id\) === 'post'[\s\S]*?&& aksrt_is_show_sidebar\(\);/);
assert.match(single, /\$show_aside = get_post_type\(\$post_id\) === 'post'[\s\S]*?&& jio_is_show_sidebar\(\);/);
assert.match(aside, /get_post_type\(\$post_id\) !== 'post' \|\| empty\(\$ctx\['show_aside'\]\)/);
// Homepage aside renders under the same single decision; no dual gate.
assert.match(homepage, /function aksrt_homepage_render_sidebar\(\): void \{\s*if \(!aksrt_is_show_sidebar\(\)\) \{\s*return;/);
assert.doesNotMatch(homepage, /aksrt_homepage_has_sidebar/);
assert.match(homepage, /function jio_homepage_render_sidebar\(\): void \{\s*if \(!jio_is_show_sidebar\(\)\) \{\s*return;/);
assert.doesNotMatch(homepage, /jio_homepage_has_sidebar/);
// Generic sidebar template early-returns instead of rendering an empty column.
assert.match(sidebarTemplate, /if \(!function_exists\('aksrt_is_show_sidebar'\) \|\| !aksrt_is_show_sidebar\(\)\) \{\s*return;/);
assert.match(sidebarTemplate, /if \(!function_exists\('jio_is_show_sidebar'\) \|\| !jio_is_show_sidebar\(\)\) \{\s*return;/);
assert.doesNotMatch(sidebarTemplate, /legacy parity/);
// Archive helper takes only the page-level toggle; no dead params.
assert.match(archiveHelpers, /function aksrt_archive_has_sidebar\(bool \$enabled = true\): bool/);
assert.match(archiveHelpers, /function jio_archive_has_sidebar\(bool \$enabled = true\): bool/);
assert.doesNotMatch(archiveHelpers, /preferred|wp_is_mobile/);
});
@@ -487,89 +487,89 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
assert.doesNotMatch(scripts, /assets\/css\/routes\/profile\.css/);
assert.doesNotMatch(scripts, /assets\/css\/routes\/user-center\.css/);
assert.match(settingsAccount, /data-aksrt-settings-modal="<\?php echo esc_attr\(\$modal\); \?>"/);
assert.match(settingsAccount, /data-jio-settings-modal="<\?php echo esc_attr\(\$modal\); \?>"/);
assert.match(settingsAccount, /\$render_row\([^;]*'email'/);
assert.match(settingsAccount, /\$render_row\([^;]*'phone'/);
assert.match(settingsAccount, /\$render_row\([^;]*'password'/);
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="password"/);
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="email"/);
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="phone"/);
assert.match(settingsAccount, /id="pwd-form"[\s\S]*?data-aksrt-settings-submit-label/);
assert.match(settingsAccount, /template data-jio-settings-modal-template="password"/);
assert.match(settingsAccount, /template data-jio-settings-modal-template="email"/);
assert.match(settingsAccount, /template data-jio-settings-modal-template="phone"/);
assert.match(settingsAccount, /id="pwd-form"[\s\S]*?data-jio-settings-submit-label/);
assert.match(settingsAccount, /id="email-new-form"[\s\S]*?but jb-blue padding-lg btn-block/);
assert.match(settingsAccount, /id="aksrt-bind-phone-form"[\s\S]*?but jb-blue padding-lg btn-block/);
assert.doesNotMatch(settingsAccount, /aksrt-settings-modal__footer|确认修改|确认更改|确认绑定/);
assert.match(settingsAccount, /id="jio-bind-phone-form"[\s\S]*?but jb-blue padding-lg btn-block/);
assert.doesNotMatch(settingsAccount, /jio-settings-modal__footer|确认修改|确认更改|确认绑定/);
assert.ok(settingsAccount.indexOf('绑定邮箱') < settingsAccount.indexOf('绑定手机'));
assert.ok(settingsAccount.indexOf('绑定手机') < settingsAccount.indexOf("require __DIR__ . '/settings-bindings.php'"));
assert.ok(settingsAccount.indexOf("require __DIR__ . '/settings-bindings.php'") < settingsAccount.indexOf('账户密码'));
assert.ok(settingsBindings.indexOf('绑定社交账号') < settingsBindings.indexOf('aksrt-account-list--social'));
assert.ok(settingsBindings.indexOf('绑定社交账号') < settingsBindings.indexOf('jio-account-list--social'));
assert.match(settingsAccount, /!empty\(\$user_settings\['bind_phone'\]\)/);
assert.match(settingsBindings, /aksrt-account-list--social/);
assert.match(settingsBindings, /jio-account-list--social/);
assert.match(settingsBindings, /binding-unbind-btn/);
assert.match(userCenterJs, /runtime\.open\(\{/);
assert.match(userCenterJs, /className: 'aksrt-user-settings-modal modal-mini full-sm'/);
assert.match(userCenterJs, /className: 'jio-user-settings-modal modal-mini full-sm'/);
assert.match(userCenterJs, /mobileBottom: true/);
assert.match(userCenterJs, /onContent: function \(_, session\)/);
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(event\.detail && event\.detail\.shell\)/);
assert.match(userCenterJs, /jio:modal:close[\s\S]*?resetSettingsModal\(event\.detail && event\.detail\.shell\)/);
assert.match(userCenterJs, /resetCaptcha\(scene, form\)/);
assert.match(userCenterJs, /function setSettingsSubmitLabel\(button, text\)[\s\S]*?label\.textContent = text/);
assert.match(userCenterJs, /aksrt_change_password/);
assert.match(userCenterJs, /aksrt_change_email/);
assert.match(userCenterJs, /jio_change_password/);
assert.match(userCenterJs, /jio_change_email/);
assert.match(settingsAccount, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"/);
assert.match(settingsAccount, /\$email_verify_required/);
assert.match(settingsAccount, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"/);
assert.match(settingsAccount, /data-account-recovery-open[\s\S]*?data-account-password-pane="recovery"/);
assert.match(settingsAccount, /aksrt_render_captcha\('change_password'\)/);
assert.match(settingsAccount, /jio_render_captcha\('change_password'\)/);
assert.match(userCenterJs, /\/auth\/code/);
assert.match(userCenterJs, /\/auth\/reset-password/);
assert.match(userCenterJs, /\/user\/phone-verify/);
assert.doesNotMatch(settingsAccount + userCenterJs + settingsCss, /pwd-strength|pwd-send-code|email-send-code|aksrt_send_email_code/);
assert.match(settingsCss, /\.aksrt-modal-runtime\.aksrt-user-settings-modal:not\(\.aksrt-modal-runtime--bottom\)[\s\S]*?width: 400px/);
assert.match(settingsCss, /\.aksrt-settings-modal \.modal-colorful-header[\s\S]*?height: 120px/);
assert.doesNotMatch(settingsAccount + userCenterJs + settingsCss, /pwd-strength|pwd-send-code|email-send-code|jio_send_email_code/);
assert.match(settingsCss, /\.jio-modal-runtime\.jio-user-settings-modal:not\(\.jio-modal-runtime--bottom\)[\s\S]*?width: 400px/);
assert.match(settingsCss, /\.jio-settings-modal \.modal-colorful-header[\s\S]*?height: 120px/);
assert.match(settingsAccount, /class="colorful-make"/);
assert.doesNotMatch(userCenterJs, /function closeModal\(|\.aksrt-modal\.is-open|security-trigger|data-close-modal/);
assert.doesNotMatch(userCenterJs, /function closeModal\(|\.jio-modal\.is-open|security-trigger|data-close-modal/);
for (const deletedPath of [
'inc/helpers/user-center-renderers/profile-account-modals.php',
'inc/helpers/user-center-renderers/settings-security.php',
'assets/css/routes/profile/modals.css',
]) assert.equal(fs.existsSync(path.join(root, deletedPath)), false, `stale settings modal file: ${deletedPath}`);
assert.doesNotMatch(profileListsCss, /aksrt-modal__icon|aksrt-modal__close|security-action/);
assert.match(userHomeCss, /\.aksrt-profile-details-modal\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/);
assert.match(userHomeCss, /\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\] \.posts-item\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/);
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\]\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\);/);
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\] > \.posts-item\s*\{[^}]*width:\s*auto;[^}]*margin:\s*0 !important;/);
assert.match(userHomeCss, /@media \(max-width: 767px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\] \.posts-item\s*\{[^}]*padding:\s*10px 0;/);
assert.doesNotMatch(profileListsCss, /jio-modal__icon|jio-modal__close|security-action/);
assert.match(userHomeCss, /\.jio-profile-details-modal\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/);
assert.match(userHomeCss, /\.jio-user-home \.jio-user-home__post-list\[data-view="list"\] \.posts-item\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/);
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.jio-user-home \.jio-user-home__post-list\[data-view="list"\]\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\);/);
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.jio-user-home \.jio-user-home__post-list\[data-view="list"\] > \.posts-item\s*\{[^}]*width:\s*auto;[^}]*margin:\s*0 !important;/);
assert.match(userHomeCss, /@media \(max-width: 767px\)\s*\{[\s\S]*?\.jio-user-home \.jio-user-home__post-list\[data-view="list"\] \.posts-item\s*\{[^}]*padding:\s*10px 0;/);
const panelFavorites = read('templates/parts/user-home/panel-favorites.php');
assert.doesNotMatch(panelFavorites, /'type'\s*=>\s*'list'/);
assert.match(panelFavorites, /'context'\s*=>\s*'author'/);
assert.match(panelFavorites, /aksrt_render_user_home_pagination\(\(int\) \$favorites_query->max_num_pages, \(int\) \$favorites_page\)/);
assert.match(panelFavorites, /array_merge\(\$user_home_render_config, \['wrapper_class' => 'aksrt-user-home__post-list', 'append_inner_html' => \$user_home_panel_loader\]\), \$favorites_query\)/);
assert.match(panelFavorites, /aksrt_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
assert.match(read('templates/parts/user-home/panel-posts.php'), /array_merge\(\$user_home_render_config, \['wrapper_class' => 'aksrt-user-home__post-list', 'append_inner_html' => \$user_home_panel_loader\]\), \$author_posts\)/);
assert.match(panelFavorites, /jio_render_user_home_pagination\(\(int\) \$favorites_query->max_num_pages, \(int\) \$favorites_page\)/);
assert.match(panelFavorites, /array_merge\(\$user_home_render_config, \['wrapper_class' => 'jio-user-home__post-list', 'append_inner_html' => \$user_home_panel_loader\]\), \$favorites_query\)/);
assert.match(panelFavorites, /jio_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
assert.match(read('templates/parts/user-home/panel-posts.php'), /array_merge\(\$user_home_render_config, \['wrapper_class' => 'jio-user-home__post-list', 'append_inner_html' => \$user_home_panel_loader\]\), \$author_posts\)/);
assert.doesNotMatch(panelFavorites, /profile-list-item|\$_GET|add_query_arg/);
// Only the two-column user center spans the viewport; the single-column public
// profile keeps following the admin content max width.
assert.match(frontendLayoutCss, /\.site-main\.content-shell\.aksrt-profile\s*\{[\s\S]*?max-width:\s*100%/);
assert.match(frontendLayoutCss, /\.site-main\.content-shell\.aksrt-user-home\s*\{[\s\S]*?max-width:\s*var\(--container, 1440px\)[\s\S]*?padding-top:\s*0/);
assert.match(frontendLayoutCss, /\.site-main\.content-shell\.jio-profile\s*\{[\s\S]*?max-width:\s*100%/);
assert.match(frontendLayoutCss, /\.site-main\.content-shell\.jio-user-home\s*\{[\s\S]*?max-width:\s*var\(--container, 1440px\)[\s\S]*?padding-top:\s*0/);
// The front end enqueues build/public.min.css, not the frontend/ sources, so a
// source-only edit never reaches the page. The bundle must carry the contract.
assert.match(publicBundleCss, /\.site-main\.content-shell\.aksrt-profile\s*\{[\s\S]*?max-width:\s*100%/);
assert.match(publicBundleCss, /\.site-main\.content-shell\.aksrt-user-home\s*\{[\s\S]*?max-width:\s*var\(--container, 1440px\)/);
// .aksrt-profile on <main> is the user center only; other routes must not carry it.
assert.equal(read('templates/pages/page-user-home.php').includes('aksrt-profile"'), false);
assert.doesNotMatch(profileLayoutCss, /\.aksrt-profile\.content-shell\s*,[\s\S]*?max-width:\s*var\(--container/);
assert.doesNotMatch(userCenterShellCss, /\.aksrt-user-center \.content-shell\s*,[\s\S]*?max-width:\s*var\(--container/);
assert.match(userCenterShellCss, /\.aksrt-profile\.content-shell\s*\{[\s\S]*?max-width:\s*none/);
assert.match(userCenterShellCss, /\.aksrt-profile\.user-center\s*\{[\s\S]*?display:\s*block;[\s\S]*?gap:\s*0/);
assert.match(userCenterShellCss, /\.aksrt-profile\.content-shell > \.content-wrap,[\s\S]*?\.aksrt-profile\.content-shell \.uc-header\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-width:\s*none/);
assert.match(profileLayoutCss, /\.aksrt-cover-menu__panel\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/);
assert.match(profileLayoutCss, /#aksrt-bg-file-input\[hidden\],[\s\S]*?#aksrt-avatar-file-input\[hidden\][\s\S]*?display:\s*none\s*!important;/);
assert.match(settingsAccount, /id="aksrt-bind-phone-form"[\s\S]*?data-aksrt-sms-trigger/);
assert.match(publicBundleCss, /\.site-main\.content-shell\.jio-profile\s*\{[\s\S]*?max-width:\s*100%/);
assert.match(publicBundleCss, /\.site-main\.content-shell\.jio-user-home\s*\{[\s\S]*?max-width:\s*var\(--container, 1440px\)/);
// .jio-profile on <main> is the user center only; other routes must not carry it.
assert.equal(read('templates/pages/page-user-home.php').includes('jio-profile"'), false);
assert.doesNotMatch(profileLayoutCss, /\.jio-profile\.content-shell\s*,[\s\S]*?max-width:\s*var\(--container/);
assert.doesNotMatch(userCenterShellCss, /\.jio-user-center \.content-shell\s*,[\s\S]*?max-width:\s*var\(--container/);
assert.match(userCenterShellCss, /\.jio-profile\.content-shell\s*\{[\s\S]*?max-width:\s*none/);
assert.match(userCenterShellCss, /\.jio-profile\.user-center\s*\{[\s\S]*?display:\s*block;[\s\S]*?gap:\s*0/);
assert.match(userCenterShellCss, /\.jio-profile\.content-shell > \.content-wrap,[\s\S]*?\.jio-profile\.content-shell \.uc-header\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-width:\s*none/);
assert.match(profileLayoutCss, /\.jio-cover-menu__panel\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/);
assert.match(profileLayoutCss, /#jio-bg-file-input\[hidden\],[\s\S]*?#jio-avatar-file-input\[hidden\][\s\S]*?display:\s*none\s*!important;/);
assert.match(settingsAccount, /id="jio-bind-phone-form"[\s\S]*?data-jio-sms-trigger/);
assert.match(settingsAccount, /class="modal-colorful-header colorful-bg jb-blue"/);
assert.match(settingsAccount, /class="modal-colorful-header colorful-bg jb-yellow"/);
assert.doesNotMatch(settingsAccount, /class="aksrt-modal"|aksrt-modal__overlay|aksrt-modal__box/);
assert.doesNotMatch(settingsAccount, /class="jio-modal"|jio-modal__overlay|jio-modal__box/);
assert.match(userCenterCreditsCss, /#panel-credits\.dashboard-panel\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/);
assert.match(userCenterCreditsCss, /\.uc-asset-tabs\s*\{[^}]*background:\s*var\(--surface-1,\s*#fff\);[^}]*border:[^;}]+;[^}]*box-shadow:[^;}]+;/);
assert.doesNotMatch(profileCredits, /credits-signin\.php|data-aksrt-signin-calendar/);
assert.doesNotMatch(profileCredits, /credits-signin\.php|data-jio-signin-calendar/);
// 签到详情弹窗为参考主题 1:1 结构(src/Credits/Signin.php),日历已随
// 旧版 lite 弹窗一并移除;用户中心里的孤儿渲染器也已整包移除。
assert.equal(fs.existsSync(path.join(root, 'inc/helpers/user-center-renderers/credits-signin.php')), false);
@@ -577,35 +577,35 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
const signinModalHtml = read('src/Credits/Signin.php');
assert.match(signinModalHtml, /modal-colorful-header colorful-bg c-blue/);
assert.match(signinModalHtml, /checkin-mini-box/);
assert.doesNotMatch(signinModalHtml, /data-aksrt-signin-calendar/);
assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden/);
assert.doesNotMatch(signinModalHtml, /data-jio-signin-calendar/);
assert.match(profileOrders, /data-jio-orders-filter-panel hidden/);
assert.doesNotMatch(profileOrders, /\$_GET|add_query_arg|name="order_(?:status|type|number|page)"/);
assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden[\s\S]*?aksrt-orders-page__status-filters[\s\S]*?aksrt-orders-page__type-filters/);
assert.match(profileOrders, /data-aksrt-order-filter="status"[\s\S]*?data-aksrt-order-filter="type"/);
assert.match(profileOrders, /data-aksrt-order-page=/);
assert.doesNotMatch(profileOrders, /aksrt-orders-page__(?:filter-chip|page-chip|clear)[^>]*href=/);
assert.doesNotMatch(profileOrders, /aksrt-orders-page__tabs|aksrt-orders-page__tab(?:\s|<|"|')/);
assert.match(ordersCss, /\.aksrt-orders-page__filter-panel\[hidden\]\s*\{\s*display:\s*none\s*!important;/);
assert.match(ordersCss, /\.aksrt-orders-page__status-filters, \.aksrt-orders-page__type-filters/);
assert.doesNotMatch(ordersCss, /\.aksrt-orders-page__tabs|\.aksrt-orders-page__tab(?:\s|\.|:|\{)/);
assert.match(profileOrders, /data-jio-orders-filter-panel hidden[\s\S]*?jio-orders-page__status-filters[\s\S]*?jio-orders-page__type-filters/);
assert.match(profileOrders, /data-jio-order-filter="status"[\s\S]*?data-jio-order-filter="type"/);
assert.match(profileOrders, /data-jio-order-page=/);
assert.doesNotMatch(profileOrders, /jio-orders-page__(?:filter-chip|page-chip|clear)[^>]*href=/);
assert.doesNotMatch(profileOrders, /jio-orders-page__tabs|jio-orders-page__tab(?:\s|<|"|')/);
assert.match(ordersCss, /\.jio-orders-page__filter-panel\[hidden\]\s*\{\s*display:\s*none\s*!important;/);
assert.match(ordersCss, /\.jio-orders-page__status-filters, \.jio-orders-page__type-filters/);
assert.doesNotMatch(ordersCss, /\.jio-orders-page__tabs|\.jio-orders-page__tab(?:\s|\.|:|\{)/);
assert.match(orderCenterJs, /if \(panel\) panel\.hidden = expanded;/);
assert.doesNotMatch(profileOrders, /can_open|entitlement_label|is-entitlement/);
assert.doesNotMatch(orderCenterJs, /can_open|entitlement_label|is-entitlement/);
assert.doesNotMatch(ordersCss, /is-entitlement/);
assert.match(orderCenterJs, /function loadView\(state, patch\)/);
assert.match(orderCenterJs, /data-aksrt-order-filter/);
assert.match(orderCenterJs, /data-aksrt-order-page/);
assert.match(orderCenterJs, /data-jio-order-filter/);
assert.match(orderCenterJs, /data-jio-order-page/);
assert.doesNotMatch(orderCenterJs, /new URL\(window\.location\.href\)/);
// 订单详情弹窗滚动发生在 runtime dialog 本体:滚动条必须完全隐藏(含细滚动条),滚动行为不变。
assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-modal-runtime__dialog\s*\{[^}]*scrollbar-width:\s*none/);
assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-modal-runtime__dialog::-webkit-scrollbar\s*\{\s*display:\s*none;\s*\}/);
assert.doesNotMatch(ordersCss, /aksrt-order-detail-modal[^{}]*mini-scrollbar/);
assert.match(ordersCss, /\.jio-order-detail-modal \.jio-modal-runtime__dialog\s*\{[^}]*scrollbar-width:\s*none/);
assert.match(ordersCss, /\.jio-order-detail-modal \.jio-modal-runtime__dialog::-webkit-scrollbar\s*\{\s*display:\s*none;\s*\}/);
assert.doesNotMatch(ordersCss, /jio-order-detail-modal[^{}]*mini-scrollbar/);
assert.doesNotMatch(userCenterJs, /\['order_status', 'order_type', 'order_number', 'order_page'\]/);
assert.match(userCenterJs, /initialTab === 'orders'[\s\S]*?window\.history\.replaceState/);
});
test('public user home stays param-free and swaps panels via REST fragments', () => {
const ctx = read('inc/helpers/aksrt-user-home.php');
const ctx = read('inc/helpers/jio-user-home.php');
const shell = read('templates/pages/page-user-home.php');
const follow = read('templates/parts/user-home/panel-follow.php');
const comments = read('templates/parts/user-home/panel-comments.php');
@@ -615,9 +615,9 @@ test('public user home stays param-free and swaps panels via REST fragments', ()
// Context builder owns no request state; pages default to 1.
assert.doesNotMatch(ctx, /\$_GET/);
assert.match(ctx, /function aksrt_render_user_home_pagination\(int \$total_pages, int \$current\): void/);
assert.match(ctx, /function jio_render_user_home_pagination\(int \$total_pages, int \$current\): void/);
assert.match(ctx, /data-uh-page/);
assert.match(ctx, /function aksrt_capture_user_home_panel\(string \$tab, array \$ctx\): string/);
assert.match(ctx, /function jio_capture_user_home_panel\(string \$tab, array \$ctx\): string/);
// Shell mounts panel wrappers from the tab registry with a user id hook.
assert.match(shell, /data-user-id="<\?php echo \(int\) \$user_id; \?>">/);
@@ -628,23 +628,23 @@ test('public user home stays param-free and swaps panels via REST fragments', ()
// Follow panel: button-driven sub-tabs, pagination without URL building.
assert.match(follow, /data-uh-sub="followers"/);
assert.match(follow, /data-uh-sub="following"/);
assert.match(follow, /aksrt_render_user_home_pagination\(\$total_pages, \$follow_page\)/);
assert.match(ctx, /function aksrt_get_user_home_follow_placeholder\(\): string[\s\S]*?author-minicard radius8 flex ac uh-follow__placeholder[\s\S]*?placeholder k1 mb6[\s\S]*?placeholder s1/);
assert.match(follow, /jio_render_user_home_pagination\(\$total_pages, \$follow_page\)/);
assert.match(ctx, /function jio_get_user_home_follow_placeholder\(\): string[\s\S]*?author-minicard radius8 flex ac uh-follow__placeholder[\s\S]*?placeholder k1 mb6[\s\S]*?placeholder s1/);
assert.match(ctx, /str_repeat\(\$item, 6\)/);
assert.match(follow, /aksrt_get_user_home_follow_placeholder\(\)/);
assert.match(follow, /jio_get_user_home_follow_placeholder\(\)/);
assert.match(follow, /data-uh-retry[\s\S]*?重试/);
assert.doesNotMatch(follow, /add_query_arg|\$_GET|paginate_links/);
assert.match(comments, /data-uh-comments-next=/);
assert.match(favorites, /aksrt_render_user_home_pagination\(/);
assert.match(favorites, /if \(!empty\(\$placeholder_only\)\)[\s\S]*?aksrt_render_placeholder\(\$placeholder_mode, 4\)/);
assert.match(favorites, /jio_render_user_home_pagination\(/);
assert.match(favorites, /if \(!empty\(\$placeholder_only\)\)[\s\S]*?jio_render_placeholder\(\$placeholder_mode, 4\)/);
assert.match(favorites, /data-uh-retry[\s\S]*?重试/);
assert.doesNotMatch(comments + favorites, /add_query_arg|\$_GET/);
// REST fragment endpoint serves public panel bodies.
assert.ok(rest.includes("'/user-home/panel/(?P<user_id>\\d+)/(?P<tab>[a-z0-9_-]+)'"));
assert.match(rest, /'permission_callback'\s*=>\s*'__return_true'/);
assert.match(rest, /aksrt_capture_user_home_panel\(\$tab, \$context\)/);
assert.match(rest, /jio_capture_user_home_panel\(\$tab, \$context\)/);
// Runtime never reads or writes URL params; fragments replace panel bodies.
assert.doesNotMatch(authorJs, /searchParams|pushState|replaceState/);
@@ -685,7 +685,7 @@ test('single articles enqueue only content and settings driven assets', () => {
assert.match(builder, /'single\.css': \[[\s\S]*?single-post\/shell\.css'[\s\S]*?single-post\/content\.css'[\s\S]*?single-post\/author-actions\.css'/);
assert.match(builder, /'comments\.css': \[[\s\S]*?comments\/shell\.css'[\s\S]*?comments\/mobile-item\.css'/);
assert.match(singleBlock, /build\/single\.min\.css/);
assert.match(singleBlock, /wp_register_style\([\s\S]*?'aksrt-single-post'[\s\S]*?array_merge\(\['aksrt-single-core'\], \$single_feature_handles\)/);
assert.match(singleBlock, /wp_register_style\([\s\S]*?'jio-single-post'[\s\S]*?array_merge\(\['jio-single-core'\], \$single_feature_handles\)/);
assert.doesNotMatch(singleBlock, /\$enqueue_module\('single-post'\)|\$enqueue_module\('comments'\)/);
for (const requirement of [
@@ -707,9 +707,9 @@ test('single articles enqueue only content and settings driven assets', () => {
assert.match(singleBlock, new RegExp(`singular_assets\\['${requirement}'\\]`));
}
assert.match(runtime, /wp:\(\?:code\|preformatted\|aksrt\\\/enlighter\)/);
assert.match(runtime, /wp:\(\?:code\|preformatted\|jio\\\/enlighter\)/);
assert.match(runtime, /\$requirements\['has_toc'\] = \$is_article && \$article_nav_enabled/);
assert.match(runtime, /data-aksrt-video/);
assert.match(runtime, /data-jio-video/);
assert.match(runtime, /post_password_required\(\$post_id\)/);
assert.match(runtime, /comment_settings\['close_comments'\]/);
assert.match(runtime, /comment_settings\['require_login'\]/);
@@ -734,9 +734,9 @@ test('single articles enqueue only content and settings driven assets', () => {
test('single post more menu has one accessible runtime and an in-flow author anchor', () => {
const article = read('assets/js/core/article.js');
const components = read('inc/helpers/aksrt-single-components.php');
const single = read('inc/helpers/aksrt-single.php');
const singleRender = read('inc/helpers/aksrt-single-render.php');
const components = read('inc/helpers/jio-single-components.php');
const single = read('inc/helpers/jio-single.php');
const singleRender = read('inc/helpers/jio-single-render.php');
const authorBar = read('templates/parts/single-author-bar.php');
const styles = read('assets/css/frontend/single-post/author-actions.css');
@@ -751,10 +751,10 @@ test('single post more menu has one accessible runtime and an in-flow author anc
assert.match(components, /\$menu_id = 'post-drop-meta-menu-' \. \$post_id/);
assert.match(components, /aria-controls="' \. esc_attr\(\$menu_id\)/);
assert.match(components, /aria-haspopup="true"/);
assert.match(single, /aksrt_get_single_meta_box\(\$post_id\)/);
assert.match(single, /jio_get_single_meta_box\(\$post_id\)/);
assert.doesNotMatch(components, /\$direction/);
assert.match(components, /\$count_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$count\) : number_format_i18n\(\$count\)/);
assert.match(components, /\$likes_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$likes\) : number_format_i18n\(\$likes\)/);
assert.match(components, /\$count_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$count\) : number_format_i18n\(\$count\)/);
assert.match(components, /\$likes_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$likes\) : number_format_i18n\(\$likes\)/);
assert.match(components, /esc_html\(\(string\) \$count_label\)/);
assert.match(components, /esc_html\(\(string\) \$likes_label\)/);
assert.match(authorBar, /class="single-meta-row"/);
@@ -767,18 +767,18 @@ test('single post more menu has one accessible runtime and an in-flow author anc
});
test('single post like, favorite and comment counts abbreviate everywhere', () => {
const actionsRender = read('inc/helpers/aksrt-single-render.php');
const actionsRender = read('inc/helpers/jio-single-render.php');
const tabbar = read('inc/helpers/template-tags/pagination-permissions.php');
const article = read('assets/js/core/article.js');
assert.match(actionsRender, /\$like_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
assert.match(actionsRender, /\$favorite_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$favorite_count\) : number_format_i18n\(\$favorite_count\)/);
assert.match(actionsRender, /\$like_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
assert.match(actionsRender, /\$favorite_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$favorite_count\) : number_format_i18n\(\$favorite_count\)/);
assert.match(actionsRender, /data-stat-count="likes" data-base-count="<\?php echo esc_attr\(\(string\) \$like_count\); \?>"><\?php echo esc_html\(\(string\) \$like_label\);/);
assert.match(actionsRender, /data-stat-count="favorites" data-base-count="<\?php echo esc_attr\(\(string\) \$favorite_count\); \?>"><\?php echo esc_html\(\(string\) \$favorite_label\);/);
assert.match(tabbar, /\$like_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
assert.match(tabbar, /\$favorite_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$favorite_count\) : number_format_i18n\(\$favorite_count\)/);
assert.match(tabbar, /\$comment_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$comment_count\) : number_format_i18n\(\$comment_count\)/);
assert.match(tabbar, /\$like_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
assert.match(tabbar, /\$favorite_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$favorite_count\) : number_format_i18n\(\$favorite_count\)/);
assert.match(tabbar, /\$comment_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$comment_count\) : number_format_i18n\(\$comment_count\)/);
assert.match(tabbar, /echo esc_html\(\(string\) \$like_label\)/);
assert.match(tabbar, /echo esc_html\(\(string\) \$favorite_label\)/);
assert.match(tabbar, /echo esc_html\(\(string\) \$comment_label\)/);
@@ -790,11 +790,11 @@ test('single post like, favorite and comment counts abbreviate everywhere', () =
});
test('abbreviated counts use uppercase unit suffixes everywhere', () => {
const frontendTags = read('inc/helpers/aksrt-frontend-tags.php');
const frontendTags = read('inc/helpers/jio-frontend-tags.php');
const article = read('assets/js/core/article.js');
const commentAjax = read('assets/js/comment-ajax.js');
const formatNumber = frontendTags.match(/function aksrt_format_number\([\s\S]*?\nendif;/)[0];
const formatNumber = frontendTags.match(/function jio_format_number\([\s\S]*?\nendif;/)[0];
assert.match(formatNumber, /round\(\$num \/ 10000, 1\) \. 'W\+'/);
assert.match(formatNumber, /round\(\$num \/ 1000, 1\) \. 'K'/);
assert.doesNotMatch(formatNumber, /\. 'k'/);
@@ -808,29 +808,29 @@ test('abbreviated counts use uppercase unit suffixes everywhere', () => {
});
test('article page sales counts and comment likes abbreviate', () => {
const pay = read('inc/helpers/aksrt-pay.php');
const pay = read('inc/helpers/jio-pay.php');
const downloadBox = read('templates/parts/resource-download-box.php');
const commentItem = read('inc/helpers/comments/item.php');
// 付费盒(文章页 + 侧栏小工具)「已售」徽章。
assert.match(pay, /\$sales_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$sales\) : number_format_i18n\(\$sales\)/);
assert.doesNotMatch(pay, /sprintf\(__\('已售 %s', 'aksrt'\), \$sales\)/);
assert.match(pay, /\$sales_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$sales\) : number_format_i18n\(\$sales\)/);
assert.doesNotMatch(pay, /sprintf\(__\('已售 %s', 'jio'\), \$sales\)/);
// 资源下载盒「已售」徽章。
assert.match(downloadBox, /\$sales_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$display_count\) : number_format_i18n\(\$display_count\)/);
assert.match(downloadBox, /printf\(esc_html__\('已售 %s', 'aksrt'\), \$sales_label\)/);
assert.match(downloadBox, /\$sales_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$display_count\) : number_format_i18n\(\$display_count\)/);
assert.match(downloadBox, /printf\(esc_html__\('已售 %s', 'jio'\), \$sales_label\)/);
assert.doesNotMatch(downloadBox, /number_format_i18n\(\$display_count\)\);/);
// 评论区点赞数。
assert.match(commentItem, /\$like_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
assert.match(commentItem, /\$like_label = function_exists\('jio_format_number'\) \? jio_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
assert.match(commentItem, /esc_html\(\(string\) \$like_label\)/);
});
test('single post has no word-count or estimated-reading-time chain', () => {
const chain = join([
'inc/helpers/aksrt-single.php',
'inc/helpers/aksrt-single-render.php',
'inc/helpers/aksrt-frontend-tags.php',
'inc/helpers/jio-single.php',
'inc/helpers/jio-single-render.php',
'inc/helpers/jio-frontend-tags.php',
'src/Settings/PostDisplay.php',
'src/Settings/FacadeBulk.php',
'src/Admin/Schema/SchemaPostList.php',
@@ -838,7 +838,7 @@ test('single post has no word-count or estimated-reading-time chain', () => {
'assets/css/frontend/single-post/shell.css',
]);
assert.doesNotMatch(chain, /aksrt_(?:post_word_count|reading_minutes)/);
assert.doesNotMatch(chain, /jio_(?:post_word_count|reading_minutes)/);
assert.doesNotMatch(chain, /(?:feature_|post_)?word_count(?:_enabled)?/);
assert.doesNotMatch(chain, /(?:feature_|post_)?reading_time(?:_enabled)?/);
assert.doesNotMatch(chain, /meta_items_secondary|article-reading-meta/);
@@ -13,7 +13,7 @@ for (const style of ['list', 'card', 'mini']) {
test(`article ${style} widget renders its saved layout directly`, () => {
const html = cases[`posts_${style}_detail`];
assert.match(html, new RegExp(`class="widget-main-post mb20 style-${style}"`));
assert.match(html, /data-widget="posts" data-instance="aksrt_widget_posts-3"/);
assert.match(html, /data-widget="posts" data-instance="jio_widget_posts-3"/);
assert.match(html, /data-loaded="1" aria-live="polite" aria-busy="false"/);
assert.equal((html.match(/<h2 class="item-heading\b/g) || []).length, 4);
assert.match(html, /data-widget-page="2"/);
@@ -26,7 +26,7 @@ for (const style of ['list', 'card', 'mini']) {
const html = cases[`posts_${style}_ajax`];
assert.match(html, new RegExp(`class="widget-main-post mb20 style-${style}"`));
assert.match(html, /class="widget-ajaxpager lazyload" data-widget-lazy/);
assert.match(html, /data-widget="posts" data-instance="aksrt_widget_posts-3"/);
assert.match(html, /data-widget="posts" data-instance="jio_widget_posts-3"/);
assert.match(html, /aria-live="polite" aria-busy="true"/);
assert.match(html, /class="post_ajax_loader is-loading" role="status"/);
assert.match(html, new RegExp(`data-placeholder="${style}:4"`));
@@ -10,7 +10,7 @@ test('article TOC widget uses the shared reference tree styles, not posts-navs p
const routeCss = read('assets/css/routes/posts-navs.css');
const mainCss = read('assets/css/main.css');
assert.doesNotMatch(routeCss, /\.posts-nav-box|\.posts-nav-lists|\.aksrt-section-nav/);
assert.doesNotMatch(routeCss, /\.posts-nav-box|\.posts-nav-lists|\.jio-section-nav/);
for (const selector of [
'.posts-nav-lists .bl',
'.posts-nav-lists li.n-H1 a',
@@ -35,6 +35,6 @@ test('mobile TOC panel and its collapse control remain styled and accessible', (
assert.match(mainCss, /\.posts-nav-lists \.nav-toggle-collapse\s*\{[^}]*border:\s*0;[^}]*background:\s*transparent;/s);
assert.match(mainCss, /\.posts-nav-box \.posts-nav-lists :where\(a, \.nav-toggle-collapse\):focus-visible/);
assert.match(mainCss, /@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.posts-nav-lists \.nav-toggle-collapse \{\s*transition:\s*none;/);
assert.doesNotMatch(mainCss, /\.mobile-navbar \.posts-nav-box \.aksrt-panel/);
assert.doesNotMatch(mainCss, /\.mobile-navbar \.posts-nav-box \.jio-panel/);
assert.doesNotMatch(read('assets/widgets/frontend.css'), /\.posts-nav-box|\.posts-nav-lists/);
});
@@ -20,7 +20,7 @@ test('TOC widget renders one escaped title and asset eligibility follows saved a
assert.match(fixture.widget, /^<div class="widget-container"><div data-affix="true" class="posts-nav-box"/);
assert.equal((fixture.widget.match(/data-title=/g) || []).length, 1);
assert.match(fixture.widget, /&amp;amp; 标题/);
assert.doesNotMatch(fixture.widget, /<aside>|class="title-theme"|aksrt-widget-shell/);
assert.doesNotMatch(fixture.widget, /<aside>|class="title-theme"|jio-widget-shell/);
});
test('article TOC renderer owns its reference container and title without the generic title wrapper', () => {
@@ -45,12 +45,12 @@ test('TOC script enqueue follows article visibility settings and per-post overri
assert.match(scripts, /empty\(\$extension_meta\['no_article-navs'\]\)/);
assert.match(scripts, /\$requirements\['has_toc'\] = \$is_article && \$article_nav_enabled/);
assert.doesNotMatch(scripts, /preg_match_all\('\/<h\[1-4\]/);
assert.match(scripts, /\$singular_assets\['has_toc'\][\s\S]*?'aksrt-section-navs'/);
assert.match(scripts, /\$singular_assets\['has_toc'\][\s\S]*?'jio-section-navs'/);
const pageScripts = read('src/Core/ScriptsPart1b.php');
const pageBlockStart = pageScripts.indexOf("if (is_page_template('templates/pages/page-posts-navs.php'))");
const pageBlockEnd = pageScripts.indexOf('// 文章归档页', pageBlockStart);
const pageBlock = pageScripts.slice(pageBlockStart, pageBlockEnd);
assert.match(pageBlock, /aksrt-posts-navs-page/);
assert.doesNotMatch(pageBlock, /aksrt-section-navs|section-navs\.js/);
assert.match(pageBlock, /jio-posts-navs-page/);
assert.doesNotMatch(pageBlock, /jio-section-navs|section-navs\.js/);
});
@@ -2,13 +2,13 @@
/** Exercise the production TOC widget and article asset resolver. */
declare(strict_types=1);
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function get_comment_display_settings(): array { return []; }
}
}
namespace AKSRT\WidgetSystem {
namespace Jio\WidgetSystem {
final class AssetManager {
public static function enqueue_for(WidgetDefinition $definition): void {}
}
@@ -21,32 +21,32 @@ namespace {
function post_password_required(int $id): bool { return false; }
function comments_open(int $id): bool { return false; }
function get_comments_number(int $id): int { return 0; }
function aksrt_resolve_single_post_display_settings(int $id): array { return $GLOBALS['toc_settings']; }
function jio_resolve_single_post_display_settings(int $id): array { return $GLOBALS['toc_settings']; }
class TocPost extends WP_Post { public string $post_content = '[dynamic_headings]'; }
$GLOBALS['aksrt_test_state']['singular'] = true;
$GLOBALS['aksrt_test_state']['current_id'] = 8;
$GLOBALS['aksrt_test_state']['current_post'] = new TocPost(8);
$GLOBALS['jio_test_state']['singular'] = true;
$GLOBALS['jio_test_state']['current_id'] = 8;
$GLOBALS['jio_test_state']['current_post'] = new TocPost(8);
$GLOBALS['toc_settings'] = [];
$GLOBALS['toc_meta'] = [];
$resolver = new class {
use \AKSRT\Core\ScriptsPart1c { resolve_singular_asset_requirements as public requirements; }
use \Jio\Core\ScriptsPart1c { resolve_singular_asset_requirements as public requirements; }
};
$cases = ['dynamic_headings' => $resolver->requirements(true, false, false)['has_toc']];
$GLOBALS['toc_settings'] = ['article_nav' => false];
$cases['global_disabled'] = $resolver->requirements(true, false, false)['has_toc'];
$GLOBALS['toc_settings'] = ['article_nav' => true];
$GLOBALS['toc_meta'][\AKSRT\Core\DomainModel::POST_META_KEY] = ['no_article-navs' => true];
$GLOBALS['toc_meta'][\Jio\Core\DomainModel::POST_META_KEY] = ['no_article-navs' => true];
$cases['post_disabled'] = $resolver->requirements(true, false, false)['has_toc'];
$GLOBALS['toc_meta'] = [];
$cases['other_page'] = $resolver->requirements(false, false, false)['has_toc'];
$definition = \AKSRT\WidgetSystem\WidgetRegistry::get('posts_toc');
$definition = \Jio\WidgetSystem\WidgetRegistry::get('posts_toc');
$cases['widget_fields'] = array_column($definition->fields(), 'id');
$widget = new \AKSRT\WidgetSystem\SchemaWidget($definition);
$widget = new \Jio\WidgetSystem\SchemaWidget($definition);
$widget->_set(2);
ob_start();
$widget->widget(['before_widget' => '<aside>', 'after_widget' => '</aside>'], ['title' => '目录 & 标题', 'mini_title' => '副标题', 'in_affix' => true]);
$cases['widget'] = ob_get_clean();
$GLOBALS['aksrt_test_state']['singular'] = false;
$GLOBALS['jio_test_state']['singular'] = false;
ob_start();
$widget->widget([], []);
$cases['non_article_widget'] = ob_get_clean();
@@ -12,7 +12,7 @@ test('article list uses the reference widget container and keeps widget behavior
const renderer = read('src/WidgetSystem/FrontendRenderer.php');
const catalog = read('src/WidgetSystem/WidgetCatalog.php');
assert.match(renderer, /\['user_card', 'author_card', 'tab_posts', 'posts'\]/);
assert.match(renderer, /\$classes = \['zib-widget-wrap', 'aksrt-' \. str_replace/);
assert.match(renderer, /\$classes = \['zib-widget-wrap', 'jio-' \. str_replace/);
assert.match(renderer, /echo '<div class="widget-container"><div>'/);
assert.match(renderer, /WidgetVisibility::device_class\(\$instance\)/);
assert.match(renderer, /!empty\(\$instance\['sidebar_affix'\]\)/);
@@ -32,6 +32,6 @@ test('article list pagination shares the reference widget rules and obsolete tri
'.widget-tab-post .posts-mini, .widget-main-post .posts-mini',
'.widget-tab-post .posts-mini-con, .widget-main-post .posts-mini-con',
'.widget-tab-post .item-meta .meta-pay, .widget-main-post .item-meta .meta-pay',
'.aksrt-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible',
'.jio-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible',
]) assert.ok(css.includes(token), `missing shared widget rule: ${token}`);
});
@@ -11,7 +11,7 @@ const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('profile comments follow the author-page row and moderation contract', () => {
const panel = read('templates/parts/user-home/panel-comments.php');
const item = read('templates/parts/content-profile-comment-item.php');
const dashboard = read('inc/helpers/aksrt-user-dashboard.php');
const dashboard = read('inc/helpers/jio-user-dashboard.php');
const userCenter = read('inc/helpers/user-center-renderers/profile-comments.php');
const styles = read('assets/css/routes/profile/lists.css');
@@ -20,8 +20,8 @@ test('profile comments follow the author-page row and moderation contract', () =
assert.match(panel, /'post_type'\s*=>\s*\['post'\]/);
assert.match(panel, /'count'\s*=>\s*true/);
assert.match(panel, /data-uh-comments-next=/);
assert.match(panel, /<\?php else : \?>\s*<\?php aksrt_render_profile_empty\(/);
assert.doesNotMatch(panel, /aksrt_render_user_home_pagination/);
assert.match(panel, /<\?php else : \?>\s*<\?php jio_render_profile_empty\(/);
assert.doesNotMatch(panel, /jio_render_user_home_pagination/);
assert.match(dashboard, /'type'\s*=>\s*'comment'[\s\S]*?'post_type'\s*=>\s*\['post'\]/);
assert.match(userCenter, /'type'\s*=>\s*'comment'[\s\S]*?'post_type'\s*=>\s*\['post'\]/);
@@ -72,12 +72,12 @@ test('commission is credited immediately with no settlement freeze', () => {
assert.doesNotMatch(attribution, /->stats\(/)
// 即时入账模型:不再有结算冻结期 / 结算 cron / settle_due 方法。
assert.doesNotMatch(ledger, /aksrt_affiliate_settle_due'?, \[\$this, 'settle_due'\]/)
assert.doesNotMatch(ledger, /wp_schedule_event\([^)]*aksrt_affiliate_settle_due/)
assert.doesNotMatch(ledger, /jio_affiliate_settle_due'?, \[\$this, 'settle_due'\]/)
assert.doesNotMatch(ledger, /wp_schedule_event\([^)]*jio_affiliate_settle_due/)
assert.doesNotMatch(ledger, /function settle_due|function settle_one/)
assert.doesNotMatch(ledger, /available_at|settled_at/)
// 遗留结算 cron 需在 init() 主动清理。
assert.match(ledger, /wp_unschedule_event\([^)]*aksrt_affiliate_settle_due/)
assert.match(ledger, /wp_unschedule_event\([^)]*jio_affiliate_settle_due/)
// 下单支付即把佣金 credit 到 COMMISSION 资金池。
assert.match(ledger, /AssetType::COMMISSION/)
@@ -96,7 +96,7 @@ test('referral codes are self-serve: no admin ops page, granted at registration'
// 用户菜单不再挂"推广码运营"页,trait 文件已删除。
const menus = read('src', 'Admin', 'Native', 'UsersMenus.php')
assert.doesNotMatch(menus, /UsersMenusReferral|aksrt_referral_codes|render_referral_codes_page/)
assert.doesNotMatch(menus, /UsersMenusReferral|jio_referral_codes|render_referral_codes_page/)
assert.equal(fs.existsSync(path.join(root, 'src', 'Admin', 'Native', 'UsersMenusReferral.php')), false)
// admin SPA 不再保留跳转路由。
@@ -202,17 +202,17 @@ test('poster modal shell is hidden at rest so it cannot block the whole page', (
// 标记、JS 查询与 CSS 钩子必须共用同一个属性,缺一即失效:
// 少了标记上的属性,JS 拿不到弹窗、CSS 也隐藏不了它。
assert.match(modal, /data-aksrt-affiliate-poster-modal/)
assert.match(script, /querySelector\('\[data-aksrt-affiliate-poster-modal\]'\)/)
assert.match(modal, /data-jio-affiliate-poster-modal/)
assert.match(script, /querySelector\('\[data-jio-affiliate-poster-modal\]'\)/)
// 服务端静态输出的 .modal.flex 必须显式隐藏。main.css 的 .flex{display:flex}
// 后加载且与 bootstrap 的 .modal{display:none} 同特异性,不在此回位就会让
// 静止态弹窗铺满全屏,叠加 .fade 的 opacity:0 形成隐形遮罩、拦截整页点击。
assert.match(css, /\.modal\[data-aksrt-affiliate-poster-modal\]\s*\{\s*display:\s*none;/)
assert.match(css, /\.modal\[data-aksrt-affiliate-poster-modal\]\.in\s*\{\s*display:\s*flex;/)
assert.match(css, /\.modal\[data-jio-affiliate-poster-modal\]\s*\{\s*display:\s*none;/)
assert.match(css, /\.modal\[data-jio-affiliate-poster-modal\]\.in\s*\{\s*display:\s*flex;/)
// 圆形按钮必须回位成 pill。credits.css 的 .uc-content .but.jb-blue 是 (0,3,0)
// 且后加载,会盖掉 main.css 的 .poster-share .but.cir,把蓝色下载钮压成方角。
assert.match(css, /\.modal\[data-aksrt-affiliate-poster-modal\] \.but\.cir\s*\{[^}]*border-radius:\s*5em/s)
assert.match(css, /\.modal\[data-jio-affiliate-poster-modal\] \.but\.cir\s*\{[^}]*border-radius:\s*5em/s)
// 36×36 与投影仍由 main.css 的 .poster-share .but.cir 提供。
const mainCss = read('assets', 'css', 'main.css')
assert.match(mainCss, /\.poster-share \.but\.cir\s*\{[^}]*width:\s*36px/s)
@@ -227,26 +227,26 @@ test('invite-code input verifies live against the public REST endpoint', () => {
assert.match(script, /function initInviteVerify\(root\)/)
assert.match(script, /input\[data-invite-verify\]/)
assert.match(script, /initInviteVerify\(root\)/)
assert.match(script, /\/wp-json\/aksrt\/v1\/invite-code\/verify\?code=/)
assert.match(script, /\/wp-json\/jio\/v1\/invite-code\/verify\?code=/)
})
test('affiliate posters declare the local QR dependency independently of withdrawal history', () => {
const scripts = read('src', 'Core', 'ScriptsPart1b.php')
assert.match(scripts, /assets\/js\/vendor\/qrcode\.min\.js/)
assert.match(scripts, /'aksrt-qrcode'/)
assert.match(scripts, /'jio-qrcode'/)
// 海报模块显式依赖本地 QR;提现历史无需 QR。
// credits.js 经收银台间接使用 QR,不能把这条支付依赖误认成海报依赖。
assert.match(scripts, /\['credits-affiliate' => \['loader_js', 'aksrt-qrcode'\]\]/)
assert.match(scripts, /'aksrt-credits'[\s\S]*?\['loader_js', 'aksrt-vip-coordinator', 'aksrt-pay-cashier'\]/)
assert.doesNotMatch(scripts, /'credits-withdraw-history' => \['loader_js', 'aksrt-qrcode'\]/)
assert.match(scripts, /\['credits-affiliate' => \['loader_js', 'jio-qrcode'\]\]/)
assert.match(scripts, /'jio-credits'[\s\S]*?\['loader_js', 'jio-vip-coordinator', 'jio-pay-cashier'\]/)
assert.doesNotMatch(scripts, /'credits-withdraw-history' => \['loader_js', 'jio-qrcode'\]/)
})
test('client IP honours the trusted-proxy filter before CDN headers', () => {
const captcha = read('inc', 'modules', 'captcha.php')
assert.match(captcha, /apply_filters\('aksrt_trusted_proxies', \[\]\)/)
assert.match(captcha, /function aksrt_ip_in_list\(string \$ip, array \$list\): bool/)
assert.match(captcha, /apply_filters\('jio_trusted_proxies', \[\]\)/)
assert.match(captcha, /function jio_ip_in_list\(string \$ip, array \$list\): bool/)
// CIDR 比较必须先检查同族地址长度,避免跨族误判。
assert.match(captcha, /strlen\(\$ip_binary\) !== strlen\(\$subnet_binary\)/)
// 过滤命中失败时直接回落 REMOTE_ADDR,不读转发头。
assert.match(captcha, /!aksrt_ip_in_list\(\$remote, \$trusted\)/)
assert.match(captcha, /!jio_ip_in_list\(\$remote, \$trusted\)/)
})
@@ -1,10 +1,10 @@
'use strict';
/**
* AksField 的 group / repeater 行身份稳定性契约。
* JioField 的 group / repeater 行身份稳定性契约。
*
* 背景:inline 展开模式用 vuedraggable 渲染行,vuedraggable 的 computeNodes 会把
* `node.key = getKey(element)`,而行 key 由 AksField 的 repeaterDragKey 提供。服务端
* `node.key = getKey(element)`,而行 key 由 JioField 的 repeaterDragKey 提供。服务端
* 历史行没有 _id,key 来自以「对象身份」为键的 WeakMap。
*
* 关键坑:行数据是 reactive 的。updateRepeaterItem 一旦把行展开成一个新对象,渲染时
@@ -20,7 +20,7 @@ const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const fieldVue = fs.readFileSync(
path.join(root, 'assets', 'admin', 'src', 'components', 'form', 'AksField.vue'),
path.join(root, 'assets', 'admin', 'src', 'components', 'form', 'JioField.vue'),
'utf8'
);
@@ -47,7 +47,7 @@ function reactiveOf(value) {
/** 取一整行声明(WeakMap / 种子计数器都没有花括号体)。 */
function declaration(source, marker) {
const start = source.indexOf(marker);
assert.notStrictEqual(start, -1, `AksField.vue 缺少声明: ${marker}`);
assert.notStrictEqual(start, -1, `JioField.vue 缺少声明: ${marker}`);
const end = source.indexOf('\n', start);
return source.slice(start, end === -1 ? undefined : end);
}
@@ -55,7 +55,7 @@ function declaration(source, marker) {
/** 按花括号配平切出一个顶层函数(这些函数体内没有字符串/注释里的孤立花括号)。 */
function functionBlock(source, marker) {
const start = source.indexOf(marker);
assert.notStrictEqual(start, -1, `AksField.vue 缺少函数: ${marker}`);
assert.notStrictEqual(start, -1, `JioField.vue 缺少函数: ${marker}`);
const open = source.indexOf('{', start);
assert.notStrictEqual(open, -1, `函数没有函数体: ${marker}`);
let depth = 0;
+100 -100
View File
@@ -22,10 +22,10 @@ const userBanJs = read('assets/js/user-ban.js')
const authorHero = read('templates/parts/user/author-hero.php')
const banBlock = read('templates/parts/user/ban-block.php')
const ucHeader = read('inc/helpers/user-center-renderers/profile-header.php')
const commentsHelper = read('inc/helpers/aksrt-comments.php')
const identityHelper = read('inc/helpers/aksrt-identity.php')
const avatarHelper = read('inc/helpers/aksrt-avatar.php')
const singleComponents = read('inc/helpers/aksrt-single-components.php')
const commentsHelper = read('inc/helpers/jio-comments.php')
const identityHelper = read('inc/helpers/jio-identity.php')
const avatarHelper = read('inc/helpers/jio-avatar.php')
const singleComponents = read('inc/helpers/jio-single-components.php')
const banAppeal = read('src/User/BanAppeal.php')
const mainCss = read('assets/css/main.css')
const mainMinCss = read('assets/css/main.min.css')
@@ -68,7 +68,7 @@ test('report center has no legacy ajax handlers or option migration', () => {
assert.ok(!reportCenter.includes('ajax_counts'))
assert.ok(!reportCenter.includes('migrate_from_option'))
assert.ok(!reportCenter.includes('maybe_migrate'))
assert.ok(!reportCenter.includes("get_option('aksrt_reports'"))
assert.ok(!reportCenter.includes("get_option('jio_reports'"))
assert.ok(!functions.includes('ReportCenter::get_instance()->init()'), 'ReportCenter init() is gone')
assert.match(reportCenter, /public function has_pending_report/)
assert.match(reportOps, /has_pending_report\(\$reporter_id, \$target_type, \$target_id\)/)
@@ -79,8 +79,8 @@ test('report processing delegates actions to the shared ReportCenter entry', ()
assert.match(reportCenter, /public function apply_action\(/)
assert.match(reportCenter, /->restrict\(\s*\$target_id,\s*\$action_taken,/)
assert.match(reportCenter, /lift_user_restriction\(\$target_id\)/)
assert.match(reportCenter, /in_array\(\$action_taken, \\AKSRT\\User\\Restriction::TYPES, true\)/)
assert.ok(!reportCenter.includes('aksrt_user_ban_state'), 'dual-track ban state meta must not be written or read')
assert.match(reportCenter, /in_array\(\$action_taken, \\Jio\\User\\Restriction::TYPES, true\)/)
assert.ok(!reportCenter.includes('jio_user_ban_state'), 'dual-track ban state meta must not be written or read')
for (const deadAction of ["'value' => 'warned'", "'value' => 'muted'", "'value' => 'restricted'", "'value' => 'banned'"]) {
assert.ok(!reportOps.includes(deadAction), `action value must map to Restriction types: ${deadAction}`)
}
@@ -96,7 +96,7 @@ test('restriction class supports lifting and kicks sessions on ban', () => {
assert.match(restriction, /public function lift_user_restriction\(int \$user_id, array \$types = \[self::TYPE_BAN, self::TYPE_SUSPEND, self::TYPE_MUTE\]\): int/)
assert.match(restriction, /WP_Session_Tokens::get_instance\(\$user_id\)->destroy_all\(\)/)
assert.ok(!restriction.includes('sync_user_restriction_meta'), 'write-only meta sync is dead code')
assert.ok(!restriction.includes('_aksrt_is_banned'))
assert.ok(!restriction.includes('_jio_is_banned'))
})
test('ban admin REST endpoints stay in sync with the SPA client', () => {
@@ -162,22 +162,22 @@ test('ban appeal approval actually lifts restrictions', () => {
test('ban identity badge occupies the public profile meta row', () => {
// 公开主页:禁封状态占据身份行元信息位(参考主题 author_header_identity),
// 不再是资料卡下方提示框(notice 链已移除)。
assert.match(userBanHelper, /function aksrt_get_user_ban_identity_badge/)
assert.match(authorHero, /aksrt_get_user_ban_identity_badge\(\$user_id\)/)
assert.match(authorHero, /aksrt-author-hero__identity/)
assert.ok(!authorHero.includes('aksrt-author-hero__notice'), 'profile notice box must stay removed')
assert.match(userBanHelper, /function jio_get_user_ban_identity_badge/)
assert.match(authorHero, /jio_get_user_ban_identity_badge\(\$user_id\)/)
assert.match(authorHero, /jio-author-hero__identity/)
assert.ok(!authorHero.includes('jio-author-hero__notice'), 'profile notice box must stay removed')
assert.ok(!authorHero.includes('profile_notice'), 'dashboard must not carry profile_notice anymore')
assert.ok(!home.includes('get_profile_notice'), 'Home::get_profile_notice must stay removed')
assert.ok(!mainCss.includes('aksrt-author-hero__notice'), 'notice box CSS must stay removed')
assert.ok(!mainCss.includes('jio-author-hero__notice'), 'notice box CSS must stay removed')
// 徽章字段(文案/颜色/图标)由 Restriction::state_badges() 统一管理。
assert.match(userBanHelper, /state_badges\(\)/)
// 死配置清理:公开提示开关与提示文案键不再进入运行时。
assert.ok(!userRuntime.includes('public_notice'), 'dead public_notice setting must stay removed')
assert.ok(!userRuntime.includes('ban_notice_text'), 'dead ban_notice_text setting must stay removed')
// 用户中心元信息入口:but 按钮(徽章 + 箭头)→ 查看禁封状态弹窗。
assert.match(userBanHelper, /function aksrt_get_user_ban_info_link/)
assert.match(userBanHelper, /data-aksrt-ban-info=/)
assert.match(ucHeader, /aksrt_get_user_ban_info_link/)
assert.match(userBanHelper, /function jio_get_user_ban_info_link/)
assert.match(userBanHelper, /data-jio-ban-info=/)
assert.match(ucHeader, /jio_get_user_ban_info_link/)
})
test('governance display state is a single source shared by every prompt surface', () => {
@@ -188,9 +188,9 @@ test('governance display state is a single source shared by every prompt surface
assert.match(restriction, /public function get_display_state\(int \$user_id\): string/)
assert.match(restriction, /public static function state_badges\(\): array/)
// 颜色统一管理:封号=红、小黑屋=黄、禁言=灰。
assert.match(restriction, /'label' => __\('违规已封号', 'aksrt'\),\s*'tone' => 'c-red'/)
assert.match(restriction, /'label' => __\('小黑屋禁封中', 'aksrt'\),\s*'tone' => 'c-yellow'/)
assert.match(restriction, /'label' => __\('禁言中', 'aksrt'\),\s*'tone' => 'c-gray'/)
assert.match(restriction, /'label' => __\('违规已封号', 'jio'\),\s*'tone' => 'c-red'/)
assert.match(restriction, /'label' => __\('小黑屋禁封中', 'jio'\),\s*'tone' => 'c-yellow'/)
assert.match(restriction, /'label' => __\('禁言中', 'jio'\),\s*'tone' => 'c-gray'/)
// 禁封信息行单一数据源(查看禁封状态弹窗/申诉弹窗/禁封页/管理端弹窗共用)。
assert.match(restriction, /public function get_ban_info\(int \$user_id\): array/)
assert.match(restriction, /'status'\s*=>/)
@@ -202,16 +202,16 @@ test('governance display state is a single source shared by every prompt surface
test('ban prompt surface mirrors the reference theme modals and block page', () => {
// 查看禁封状态弹窗(1:1 zib_get_ban_info_modal:多彩头 + 信息行 + 申诉按钮)。
assert.match(userBanHelper, /function aksrt_get_user_ban_info_modal/)
assert.match(userBanHelper, /aksrt-user-ban-info-modal/)
assert.match(userBanHelper, /function jio_get_user_ban_info_modal/)
assert.match(userBanHelper, /jio-user-ban-info-modal/)
assert.match(userBanHelper, /您已解封,请刷新页面/)
assert.match(userBanHelper, /您当前已无法进行申诉/)
// 禁封申诉弹窗(1:1 zib_get_user_ban_appeal_modal:edit-header + 信息框 +
// 资料项 inputs + 审核中态 + 禁止申诉态;仅本人)。
assert.match(userBanHelper, /function aksrt_get_user_ban_appeal_modal/)
assert.match(userBanHelper, /aksrt-user-ban-appeal-modal/)
assert.match(userBanHelper, /function aksrt_get_user_ban_appeal_link/)
assert.match(userBanHelper, /data-aksrt-ban-appeal=/)
assert.match(userBanHelper, /function jio_get_user_ban_appeal_modal/)
assert.match(userBanHelper, /jio-user-ban-appeal-modal/)
assert.match(userBanHelper, /function jio_get_user_ban_appeal_link/)
assert.match(userBanHelper, /data-jio-ban-appeal=/)
assert.match(userBanHelper, /抱歉!您当前已无法进行申诉/)
assert.match(userBanHelper, /您的申诉正在审核中,请耐心等待/)
assert.match(userBanHelper, /name="data\[/)
@@ -220,7 +220,7 @@ test('ban prompt surface mirrors the reference theme modals and block page', ()
// 说明框保持 rows=3 自然高度(禁封弹窗留言框才有 autoHeight/maxHeight)。
assert.match(userBanHelper, /scroll-y no-scrollbar max-vh5/)
assert.ok(!userBanHelper.includes('mini-scrollbar'), 'ban modals must not show any scrollbar (thin included)')
const appealModalFn = userBanHelper.slice(userBanHelper.indexOf('function aksrt_get_user_ban_appeal_modal'))
const appealModalFn = userBanHelper.slice(userBanHelper.indexOf('function jio_get_user_ban_appeal_modal'))
const appealModalBody = appealModalFn.slice(0, appealModalFn.indexOf('\n}'))
const appealTextarea = (appealModalBody.match(/<textarea[^>]*>/) || [''])[0]
assert.ok(appealTextarea !== '' && appealTextarea.includes('rows="3"'), 'appeal textarea keeps natural rows=3 height')
@@ -229,7 +229,7 @@ test('ban prompt surface mirrors the reference theme modals and block page', ()
// JS:autoHeight 仅对显式声明的文本框生效。
assert.match(userBanJs, /querySelectorAll\('textarea\[autoHeight\], textarea\[maxHeight\]'\)/)
// 用户中心元信息按钮:外层 but 不重复状态色,颜色仅由内层 badg 承载。
const infoLinkFn = userBanHelper.slice(userBanHelper.indexOf('function aksrt_get_user_ban_info_link'))
const infoLinkFn = userBanHelper.slice(userBanHelper.indexOf('function jio_get_user_ban_info_link'))
const infoLinkBody = infoLinkFn.slice(0, infoLinkFn.indexOf('\n}'))
assert.match(infoLinkBody, /\$classes = trim\('but p2-10'\s*\./)
assert.ok(!infoLinkBody.includes("'but p2-10 ' . (string) \$state['tone']"), 'outer but must not repeat the badge tone')
@@ -237,49 +237,49 @@ test('ban prompt surface mirrors the reference theme modals and block page', ()
assert.match(banAppeal, /is_array\(\$_POST\['data'\]\)/)
assert.match(banAppeal, /申诉说明/)
// 禁封记录弹窗 + 记录链接(1:1 zib_get_ban_log_modal / zib_get_user_ban_log_link)。
assert.match(userBanHelper, /function aksrt_get_user_ban_log_modal/)
assert.match(userBanHelper, /function aksrt_get_user_ban_log_link/)
assert.match(userBanHelper, /data-aksrt-ban-log=/)
assert.match(userBanHelper, /function jio_get_user_ban_log_modal/)
assert.match(userBanHelper, /function jio_get_user_ban_log_link/)
assert.match(userBanHelper, /data-jio-ban-log=/)
assert.match(userBanHelper, /当前用户已有%d次禁封记录,请谨慎操作/)
assert.match(userBanHelper, /该用户共有%d次禁封记录/)
// 登录后禁封状态页(1:1 zib_user_ban_restrict_login / zib_die_page)。
assert.match(userBanHelper, /function aksrt_render_user_ban_block_page/)
assert.match(userBanHelper, /function jio_render_user_ban_block_page/)
assert.match(banBlock, /抱歉,您的账号当前处于禁封状态,暂时无法登录/)
assert.match(banBlock, /退出登录/)
assert.match(banBlock, /aksrt_get_user_ban_appeal_link/)
assert.match(banBlock, /jio_get_user_ban_appeal_link/)
for (const row of ['用户名', '状态', '开始时间', '结束时间', '原因', '说明', '申诉权限']) {
assert.ok(banBlock.includes(`__('${row}', 'aksrt')`), `ban block page missing row: ${row}`)
assert.ok(banBlock.includes(`__('${row}', 'jio')`), `ban block page missing row: ${row}`)
}
// Restriction 以 template_redirect 收口(不禁登录)。
assert.match(restriction, /add_action\('template_redirect', \[\$this, 'restrict_frontend_access'\], 1\)/)
assert.match(restriction, /public function restrict_frontend_access\(\): void/)
assert.ok(!restriction.includes('enforce_login_ban'), 'login-time ban block must stay removed')
// 评论拒绝态(参考主题 zib_get_user_ban_nocan_info)。
assert.match(userBanHelper, /function aksrt_get_user_ban_nocan_html/)
assert.match(commentsHelper, /aksrt_get_user_ban_nocan_html/)
assert.match(userBanHelper, /function jio_get_user_ban_nocan_html/)
assert.match(commentsHelper, /jio_get_user_ban_nocan_html/)
assert.match(commentsHelper, /暂时无法发表评论/)
// 昵称徽章 + 头像角标(参考主题 user_name_badge / user_avatar_badge)。
assert.match(userBanHelper, /function aksrt_get_user_ban_name_badge/)
assert.match(userBanHelper, /function aksrt_get_user_ban_avatar_badge/)
assert.match(identityHelper, /aksrt_get_user_ban_name_badge\(\$user_id\)/)
assert.match(avatarHelper, /aksrt_get_user_ban_avatar_badge\(\$user_id\)/)
assert.match(userBanHelper, /function jio_get_user_ban_name_badge/)
assert.match(userBanHelper, /function jio_get_user_ban_avatar_badge/)
assert.match(identityHelper, /jio_get_user_ban_name_badge\(\$user_id\)/)
assert.match(avatarHelper, /jio_get_user_ban_avatar_badge\(\$user_id\)/)
// 三个弹窗 REST 路由(modal-mini 几何 / 每次刷新)。
for (const route of ['/user-ban/info-modal', '/user-ban/appeal-modal', '/user-ban/log-modal']) {
assert.ok(userController.includes(`'${route}'`), `missing REST route: ${route}`)
}
// 前端委托与 admin-ajax 提交。
for (const attr of ['data-aksrt-ban-info', 'data-aksrt-ban-appeal', 'data-aksrt-ban-log']) {
for (const attr of ['data-jio-ban-info', 'data-jio-ban-appeal', 'data-jio-ban-log']) {
assert.ok(userBanJs.includes(attr), `missing JS delegation: ${attr}`)
}
assert.match(userBanJs, /aksrt_ban_appeal_submit/)
assert.match(userBanJs, /jio_ban_appeal_submit/)
assert.match(userBanJs, /new FormData\(form\)/)
// modal-mini 几何(400px)与头部样式覆盖三个新弹窗类。
for (const cls of ['aksrt-user-ban-info-modal', 'aksrt-user-ban-appeal-modal', 'aksrt-user-ban-log-modal']) {
for (const cls of ['jio-user-ban-info-modal', 'jio-user-ban-appeal-modal', 'jio-user-ban-log-modal']) {
assert.ok(modalRuntimeCss.includes(cls), `missing modal geometry for ${cls}`)
}
// 头像角标样式:源文件与入队的 min 产物同步。
for (const css of [mainCss, mainMinCss]) {
assert.ok(css.includes('aksrt-avatar-ban-badge'), 'ban avatar badge CSS must exist in both main.css and main.min.css')
assert.ok(css.includes('jio-avatar-ban-badge'), 'ban avatar badge CSS must exist in both main.css and main.min.css')
}
// 申诉页面模板与专属资产已移除(死代码清理)。
assert.ok(!templatesMap.includes('page-ban-appeal'), 'ban appeal page template registration must stay removed')
@@ -288,10 +288,10 @@ test('ban prompt surface mirrors the reference theme modals and block page', ()
test('report entry hides targets already banned or suspended', () => {
// 参考主题 zib_get_report_link:已封禁/小黑屋用户不展示举报入口(禁言不隐藏)。
assert.match(userBanHelper, /function aksrt_is_user_ban_restricted/)
assert.match(commentItem, /aksrt_is_user_ban_restricted\(\$author_id\)/)
assert.match(singleComponents, /aksrt_is_user_ban_restricted\(\$post_author_id\)/)
assert.match(authorHero, /aksrt_is_user_ban_restricted\(\$user_id\)/)
assert.match(userBanHelper, /function jio_is_user_ban_restricted/)
assert.match(commentItem, /jio_is_user_ban_restricted\(\$author_id\)/)
assert.match(singleComponents, /jio_is_user_ban_restricted\(\$post_author_id\)/)
assert.match(authorHero, /jio_is_user_ban_restricted\(\$user_id\)/)
})
test('banned users may log in and meet the frontend block page', () => {
@@ -313,18 +313,18 @@ test('banned users may log in and meet the frontend block page', () => {
// Toast 透传(一般性修复,保留):REST 层不得把失败原因硬编码为凭证错误。
assert.match(phoneOps, /\$result\['error'\]/)
assert.ok(
phoneOps.includes("$login_error !== '' ? $login_error : __('账号或密码错误', 'aksrt')"),
phoneOps.includes("$login_error !== '' ? $login_error : __('账号或密码错误', 'jio')"),
'auth_login must pass through the real failure reason'
)
})
test('admin report notification links to the real page', () => {
assert.match(mailNotifications, /users\.php\?page=aksrt_user_ban'/)
assert.ok(!mailNotifications.includes('page=aksrt_reports&'), 'dead admin page link')
assert.match(mailNotifications, /users\.php\?page=jio_user_ban'/)
assert.ok(!mailNotifications.includes('page=jio_reports&'), 'dead admin page link')
assert.ok(!mailNotifications.includes('tab=reports'), 'merged ticket page has no reports tab')
// 处理说明必须随 aksrt_report_status_changed 传入邮件载荷(此前硬编码空串,
// 处理说明必须随 jio_report_status_changed 传入邮件载荷(此前硬编码空串,
// 举报人收到的“说明:”永远为空)。
assert.match(reportCenter, /do_action\('aksrt_report_status_changed', \$report_id[\s\S]*?new_status, \$note_clean\)/)
assert.match(reportCenter, /do_action\('jio_report_status_changed', \$report_id[\s\S]*?new_status, \$note_clean\)/)
assert.match(mailNotifications, /function report_status_changed\(int \$report_id, int \$reporter_id, string \$status, string \$note = ''\)/)
assert.match(mailNotifications, /'note'\s*=>\s*\$note !== '' \? \$note :/)
// 举报通知必须入站内消息系统:提交→管理员(直达工单列表)、
@@ -332,7 +332,7 @@ test('admin report notification links to the real page', () => {
assert.match(reportCenter, /notify_admins_new_report\(/)
assert.match(reportCenter, /notify_reporter_status_changed\(/)
assert.match(reportCenter, /Message::get_instance\(\)->send\(/)
assert.match(reportCenter, /users\.php\?page=aksrt_user_ban&type=report&status=pending/)
assert.match(reportCenter, /users\.php\?page=jio_user_ban&type=report&status=pending/)
})
test('native ban page uses the scoped template-and-dialog admin flow', () => {
@@ -344,8 +344,8 @@ test('native ban page uses the scoped template-and-dialog admin flow', () => {
const banCss = read('assets/css/admin/user-ban.css');
assert.match(usersMenus, /use UsersMenusBan;/);
assert.match(usersMenus, /admin_post_aksrt_users_update_report/);
assert.match(usersMenus, /admin_post_aksrt_users_process_appeal/);
assert.match(usersMenus, /admin_post_jio_users_update_report/);
assert.match(usersMenus, /admin_post_jio_users_process_appeal/);
assert.doesNotMatch(usersMenus, /use UsersMenusTickets;/, 'ban review must not use the deleted ticket renderer');
assert.ok(!fs.existsSync(path.join(root, 'src/Admin/Native/UsersMenusTickets.php')));
@@ -357,17 +357,17 @@ test('native ban page uses the scoped template-and-dialog admin flow', () => {
assert.match(banQuery, /array_slice\(\$filtered, \(\$page - 1\) \* self::BAN_PER_PAGE, self::BAN_PER_PAGE\)/);
assert.match(banQuery, /'counts'\s*=>\s*\$counts/);
assert.match(banPage, /if \(\$hook !== 'users_page_aksrt_user_ban'\) return/);
assert.match(banPage, /wp_enqueue_style\('aksrt-admin-user-ban'.*NativeUi::STYLE_HANDLE/);
assert.match(banPage, /wp_enqueue_script\('aksrt-admin-user-ban'/);
assert.match(banPage, /<dialog id="aksrt-ban-dialog"/);
assert.match(banDialogs, /<template id="aksrt-ban-detail-/);
assert.match(banPage, /data-aksrt-ban-view=/);
assert.match(banPage, /if \(\$hook !== 'users_page_jio_user_ban'\) return/);
assert.match(banPage, /wp_enqueue_style\('jio-admin-user-ban'.*NativeUi::STYLE_HANDLE/);
assert.match(banPage, /wp_enqueue_script\('jio-admin-user-ban'/);
assert.match(banPage, /<dialog id="jio-ban-dialog"/);
assert.match(banDialogs, /<template id="jio-ban-detail-/);
assert.match(banPage, /data-jio-ban-view=/);
assert.doesNotMatch(banPage, /update_user_meta|update_status|do_action\(/);
assert.doesNotMatch(banPage, /data-aksrt-ticket-toggle|data-aksrt-ticket-cancel|data-aksrt-ban-toggle/);
assert.doesNotMatch(banPage, /data-jio-ticket-toggle|data-jio-ticket-cancel|data-jio-ban-toggle/);
assert.match(banDialogs, /if \(\$pending\) \{[\s\S]*?<form method="post"/);
assert.match(banDialogs, /else \{[\s\S]*?<div class="aksrt-ban-dialog__frame">/);
assert.match(banDialogs, /else \{[\s\S]*?<div class="jio-ban-dialog__frame">/);
assert.match(banDialogs, /'action'\s*=>\s*\$action/);
assert.match(banDialogs, /record_version/);
assert.match(banDialogs, /wp_nonce_field\(\$action \. '_' \. \(int\) \$row\['id'\]\)/);
@@ -382,8 +382,8 @@ test('native ban page uses the scoped template-and-dialog admin flow', () => {
for (const method of ['handle_update_report', 'handle_process_appeal']) {
assert.match(banActions, new RegExp(`public function ${method}\\(\\): void`));
}
assert.match(banActions, /check_admin_referer\('aksrt_users_update_report_'/);
assert.match(banActions, /check_admin_referer\('aksrt_users_process_appeal_'/);
assert.match(banActions, /check_admin_referer\('jio_users_update_report_'/);
assert.match(banActions, /check_admin_referer\('jio_users_process_appeal_'/);
assert.match(banActions, /hash_equals\(\$this->ban_record_version\(\$row\), \$submitted\)/);
assert.match(banActions, /\$this->ban_request_is_current\(\$row, 'pending'\)/);
assert.match(banActions, /\$new_status === 'dismissed' && \$note === ''/);
@@ -394,7 +394,7 @@ test('native ban page uses the scoped template-and-dialog admin flow', () => {
assert.match(banJs, /window\.addEventListener\('pageshow'/);
assert.match(banJs, /previousOpener\.focus\(\)/);
assert.match(banJs, /if \(!submitting\) dialog\.close\(\)/);
assert.match(banCss, /html\.aksrt-ban-modal-open \{ overflow: hidden/);
assert.match(banCss, /html\.jio-ban-modal-open \{ overflow: hidden/);
});
test('report modal uses accessible reasons, automatic target association and optional evidence', () => {
const scripts = read('src/Core/ScriptsPart1c.php')
@@ -404,10 +404,10 @@ test('report modal uses accessible reasons, automatic target association and opt
assert.match(scripts, /report_settings\(\)/)
const reportJs = read('assets/js/report.js')
for (const part of ['aksrt-report-target', 'name="report_reason"', 'type="radio"', '其他原因', '补充说明', '图片举证', '提交举报', '关联对象:']) {
for (const part of ['jio-report-target', 'name="report_reason"', 'type="radio"', '其他原因', '补充说明', '图片举证', '提交举报', '关联对象:']) {
assert.ok(reportJs.includes(part), `report.js missing form contract: ${part}`)
}
assert.doesNotMatch(reportJs, /report_url|违规链接|data-aksrt-report-reason=|bindAutoHeight/)
assert.doesNotMatch(reportJs, /report_url|违规链接|data-jio-report-reason=|bindAutoHeight/)
for (const source of [reportJs, commentItem, authorHero]) assert.doesNotMatch(source, /target_avatar/)
assert.match(reportJs, /description\) content\.push\(description\)/)
assert.doesNotMatch(reportJs, /category\.slug|reason = category/)
@@ -415,37 +415,37 @@ test('report modal uses accessible reasons, automatic target association and opt
assert.match(reportCenter, /'category'\s*=>\s*\$category/)
assert.doesNotMatch(reportCenter, /sanitize_key\(\$category\)/)
assert.match(reportJs, /report\/upload-image/)
assert.match(reportJs, /window\.AksrtModalRuntime\.getActive\(\) === shell/)
assert.match(reportJs, /window\.JioModalRuntime\.getActive\(\) === shell/)
assert.match(userController, /'\/report\/upload-image'/)
const css = read('assets/css/frontend/report.css')
assert.match(css, /\.aksrt-report-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /\.jio-report-modal:not\(\.jio-modal-runtime--bottom\) \.jio-modal-runtime__dialog/)
assert.match(css, /600px/)
assert.match(css, /:focus-visible/)
assert.match(css, /\.aksrt-report-reason:has\(input:checked\)/)
assert.match(css, /\.jio-report-reason:has\(input:checked\)/)
assert.match(css, /clip-path: inset\(50%\)/)
assert.match(css, /text-overflow: ellipsis/)
assert.doesNotMatch(reportJs, /aksrt-report-target__icon|无需填写链接|正在提交举报,请稍候/)
assert.doesNotMatch(reportJs, /jio-report-target__icon|无需填写链接|正在提交举报,请稍候/)
assert.match(css, /safe-area-inset-bottom/)
assert.match(css, /\[hidden\] \{ display: none !important;/)
assert.doesNotMatch(modalRuntimeCss, /aksrt-report-modal/, 'retired report styles must not remain in the shared stylesheet')
assert.doesNotMatch(modalRuntimeCss, /jio-report-modal/, 'retired report styles must not remain in the shared stylesheet')
assert.match(scriptsPart1b, /assets\/css\/frontend\/report\.css/)
})
test('comments expose a report entry for other users when reporting is enabled', () => {
// 举报位于“回复”右侧三点下拉菜单内(参考主题同位置),载荷走 wp_json_encode。
assert.match(commentItem, /data-aksrt-report="/)
assert.match(commentItem, /function aksrt_get_comment_report_html/)
assert.match(commentItem, /data-jio-report="/)
assert.match(commentItem, /function jio_get_comment_report_html/)
assert.match(commentItem, /comment-report-link/)
assert.match(commentItem, /report'\]\['enabled'\]/)
assert.match(commentItem, /\$author_id === \$reporter_id/)
// 下拉菜单(置顶/编辑/批准/删除 + 禁封或举报)末项走禁封优先、否则举报。
assert.match(commentItem, /function aksrt_get_comments_action_dropdown_html[\s\S]*?\$lists \.= aksrt_get_comment_ban_or_report_item/)
assert.match(commentItem, /function jio_get_comments_action_dropdown_html[\s\S]*?\$lists \.= jio_get_comment_ban_or_report_item/)
// 禁封项(管理员可见):参考主题 comments_action_lists 钩子语义,渲染于 user-ban.php。
assert.match(userBanHelper, /comment-ban-link/)
assert.match(userBanHelper, /data-aksrt-ban="/)
assert.match(userBanHelper, /data-jio-ban="/)
// 举报不作为 comt-meta 独立按钮直接渲染。
assert.doesNotMatch(commentItem, /\$report\s*=\s*aksrt_get_comment_report_html\(\$comment\)/)
assert.doesNotMatch(commentItem, /\$report\s*=\s*jio_get_comment_report_html\(\$comment\)/)
})
test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal 1:1', () => {
@@ -459,21 +459,21 @@ test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal
assert.ok(restriction.includes(label), `ban status label missing from Restriction: ${label}`)
}
// 时长选项 1/7/14/30/100/永久(默认 7 天);类型映射:小黑屋→suspend,禁封→ban。
assert.match(userBanHelper, /1 => __\('1天', 'aksrt'\)/)
assert.match(userBanHelper, /7 => __\('7天', 'aksrt'\)/)
assert.match(userBanHelper, /100 => __\('100天', 'aksrt'\)/)
assert.match(userBanHelper, /1 => __\('1天', 'jio'\)/)
assert.match(userBanHelper, /7 => __\('7天', 'jio'\)/)
assert.match(userBanHelper, /100 => __\('100天', 'jio'\)/)
assert.match(userBanHelper, /7 === \$days \? ' checked="checked"' : ''/)
// 预置原因来自 UserRuntime::ban_settings(与后台同一数据源)。
assert.match(userBanHelper, /UserRuntime::ban_settings\(\)\['reasons'\]/)
// 勾选禁止申诉必须填写说明(参考主题同校验),落用户 meta 供申诉入口拦截。
assert.match(userBanHelper, /_aksrt_no_appeal|no_appeal/)
assert.match(userBanHelper, /_jio_no_appeal|no_appeal/)
const appeal = read('src/User/BanAppeal.php')
assert.match(appeal, /_aksrt_no_appeal/)
// 弹窗入口按钮契约:[data-aksrt-ban] 委托(封面按钮 / 评论菜单项 / 文章作者菜单项)。
assert.match(userBanHelper, /data-aksrt-ban="/)
assert.match(userBanHelper, /function aksrt_get_user_cover_menu_action_html/)
assert.match(userBanHelper, /function aksrt_get_comment_ban_or_report_item/)
assert.match(userBanHelper, /function aksrt_get_post_author_ban_item/)
assert.match(appeal, /_jio_no_appeal/)
// 弹窗入口按钮契约:[data-jio-ban] 委托(封面按钮 / 评论菜单项 / 文章作者菜单项)。
assert.match(userBanHelper, /data-jio-ban="/)
assert.match(userBanHelper, /function jio_get_user_cover_menu_action_html/)
assert.match(userBanHelper, /function jio_get_comment_ban_or_report_item/)
assert.match(userBanHelper, /function jio_get_post_author_ban_item/)
// 权限模型(参考主题 zib_current_user_can('set_user_ban') 语义):
// manage_options + 非本人 + 非超级管理员 + 功能开关。
assert.match(userBanHelper, /current_user_can\('manage_options'\)/)
@@ -487,31 +487,31 @@ test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal
assert.match(userBanHelper, /modal-colorful-header colorful-bg c-yellow/)
// 提交后整页刷新(禁封 reload:true)/ 仅关闭弹窗(解封 hide_modal:true)。
assert.match(userBanJs, /window\.location\.reload\(\)/)
assert.match(userBanJs, /AksrtModalRuntime\.close\('submit'\)/)
assert.match(userBanJs, /data-aksrt-ban-submit/)
assert.match(userBanJs, /data-aksrt-unban-submit/)
assert.match(userBanJs, /JioModalRuntime\.close\('submit'\)/)
assert.match(userBanJs, /data-jio-ban-submit/)
assert.match(userBanJs, /data-jio-unban-submit/)
// 桌面 600px(参考主题未传 data-class → Bootstrap .modal-dialog 默认宽)
// + min-height 360px(data-height=360);移动端底部抽屉。
assert.match(userBanJs, /MODAL_HEIGHT = 360/)
assert.match(userBanJs, /mobileBottom: true/)
const css = read('assets/css/frontend/modal-runtime.css')
assert.match(css, /\.aksrt-user-ban-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /\.jio-user-ban-modal:not\(\.jio-modal-runtime--bottom\) \.jio-modal-runtime__dialog/)
assert.match(css, /600px/)
// textarea autoHeight 1:1(参考主题 maxHeight 锁高 + 边框补偿)。
assert.match(userBanJs, /getAttribute\('maxHeight'\)/)
// 三个入口模板接线:封面三点菜单 / 评论区三点菜单 / 文章作者 metabox 三点菜单。
const authorHero = read('templates/parts/user/author-hero.php')
assert.match(authorHero, /aksrt_get_user_cover_menu_action_html\(\$user_id\)/)
assert.match(commentItem, /aksrt_get_comment_ban_or_report_item\(\$comment\)/)
const singleComponents = read('inc/helpers/aksrt-single-components.php')
assert.match(singleComponents, /aksrt_get_post_author_ban_item\(/)
assert.match(authorHero, /jio_get_user_cover_menu_action_html\(\$user_id\)/)
assert.match(commentItem, /jio_get_comment_ban_or_report_item\(\$comment\)/)
const singleComponents = read('inc/helpers/jio-single-components.php')
assert.match(singleComponents, /jio_get_post_author_ban_item\(/)
// 脚本入队(singular + profile,与 report.js 同组)。
const scripts = read('src/Core/ScriptsPart1b.php')
assert.match(scripts, /'aksrt-user-ban'/)
assert.match(scripts, /'jio-user-ban'/)
})
test('uninstall cleans up every ban/report meta key written by the theme', () => {
for (const key of ['_aksrt_active_restrictions', '_aksrt_is_banned', '_aksrt_no_appeal', 'aksrt_user_ban_state']) {
for (const key of ['_jio_active_restrictions', '_jio_is_banned', '_jio_no_appeal', 'jio_user_ban_state']) {
assert.ok(uninstall.includes(`'${key}'`), `missing uninstall cleanup: ${key}`)
}
})
@@ -539,8 +539,8 @@ test('report modal delegates its labelled title, close button and focus return t
const reportJs = read('assets/js/report.js')
assert.match(reportJs, /title: TARGETS\[data\.target_type\]\.title/)
assert.match(reportJs, /mobileBottom: true, focusReturn: opener/)
assert.match(reportJs, /<label class="aksrt-report-label" for=/)
assert.match(reportJs, /data-aksrt-report-reason-error role="alert"/)
assert.match(reportJs, /<label class="jio-report-label" for=/)
assert.match(reportJs, /data-jio-report-reason-error role="alert"/)
assert.doesNotMatch(reportJs, /class="close"|style="display:|setTimeout/)
})
@@ -112,24 +112,24 @@ function createRuntime({ imgEnabled = true, categories = [
if (!shell) return;
if (activeModal === shell) activeModal = null;
shell.remove();
document.dispatch('aksrt:modal:close', { detail: { shell } });
document.dispatch('jio:modal:close', { detail: { shell } });
},
};
const window = {
aksrtData: {
apiUrl: '/wp-json/aksrt/v1',
jioData: {
apiUrl: '/wp-json/jio/v1',
restNonce: 'test-nonce',
reportCategories: categories,
report: { imgEnabled },
},
AksrtModalRuntime: modalRuntime,
JioModalRuntime: modalRuntime,
fetch,
};
const jquery = () => ({
off() { return this; },
on(event, callback) {
if (event === 'auto_fun.aksrt-report') autoInit = callback;
if (event === 'auto_fun.jio-report') autoInit = callback;
return this;
},
});
@@ -157,7 +157,7 @@ function createRuntime({ imgEnabled = true, categories = [
get activeModal() { return activeModal; },
openReport(target = { target_type: 'post', target_id: 42, target_name: 'Article' }) {
const opener = document.createElement('button');
opener.setAttribute('data-aksrt-report', JSON.stringify(target));
opener.setAttribute('data-jio-report', JSON.stringify(target));
document.body.appendChild(opener);
const event = opener.dispatch('click');
return { ...modals.at(-1), opener, event };
@@ -175,7 +175,7 @@ function selectReason(modal, value) {
}
function submit(modal) {
return modal.shell.querySelector('.aksrt-report-form').dispatch('submit');
return modal.shell.querySelector('.jio-report-form').dispatch('submit');
}
function callsFor(runtime, endpoint) {
@@ -195,8 +195,8 @@ test('opens from an article button, associates the target, omits URL input, and
const runtime = createRuntime({ categories: [{ name: '<svg onload="run()">reason</svg>' }] });
const targetName = '<img src=x onerror="run()"> &';
const modal = runtime.openReport({ target_type: 'post', target_id: 73, target_name: targetName });
const target = modal.shell.querySelector('.aksrt-report-target__name');
const reason = modal.shell.querySelector('.aksrt-report-reason span');
const target = modal.shell.querySelector('.jio-report-target__name');
const reason = modal.shell.querySelector('.jio-report-reason span');
assert.equal(modal.event.defaultPrevented, true);
assert.equal(modal.options.title, '举报文章');
@@ -207,18 +207,18 @@ test('opens from an article button, associates the target, omits URL input, and
assert.equal(reason.querySelector('svg'), null);
assert.equal(modal.shell.querySelector('[name="report_url"]'), null);
assert.equal(modal.shell.querySelector('input[type="url"]'), null);
assert.ok(modal.shell.querySelector('.aksrt-report-target'));
assert.equal(modal.shell.querySelector('.aksrt-report-target__label').textContent, '关联对象:');
assert.equal(modal.shell.querySelector('.aksrt-report-target__icon'), null);
assert.ok(modal.shell.querySelector('.jio-report-target'));
assert.equal(modal.shell.querySelector('.jio-report-target__label').textContent, '关联对象:');
assert.equal(modal.shell.querySelector('.jio-report-target__icon'), null);
assert.equal(target.getAttribute('title'), target.textContent);
});
test('requires a radio reason and other text, then sends canonical fields with description', async () => {
const runtime = createRuntime();
const modal = runtime.openReport();
const form = modal.shell.querySelector('.aksrt-report-form');
const reasonError = form.querySelector('[data-aksrt-report-reason-error]');
const otherBox = form.querySelector('.aksrt-report-other');
const form = modal.shell.querySelector('.jio-report-form');
const reasonError = form.querySelector('[data-jio-report-reason-error]');
const otherBox = form.querySelector('.jio-report-other');
const otherInput = form.querySelector('[name="reason_other"]');
const description = form.querySelector('[name="report_desc"]');
@@ -265,7 +265,7 @@ test('preset reason ignores hidden custom text while including description in re
description.value = 'More context';
selectReason(modal, 1);
assert.equal(modal.shell.querySelector('.aksrt-report-other').hidden, true);
assert.equal(modal.shell.querySelector('.jio-report-other').hidden, true);
assert.equal(otherInput.disabled, true);
submit(modal);
const payload = payloadFor(callsFor(runtime, '/report/submit')[0]);
@@ -308,24 +308,24 @@ test('omits all image controls when image evidence is disabled', () => {
const runtime = createRuntime({ imgEnabled: false });
const modal = runtime.openReport();
assert.equal(modal.shell.querySelector('[data-aksrt-report-file]'), null);
assert.equal(modal.shell.querySelector('[data-aksrt-report-add]'), null);
assert.equal(modal.shell.querySelector('[data-aksrt-report-images]'), null);
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]'), null);
assert.equal(modal.shell.querySelector('[data-jio-report-file]'), null);
assert.equal(modal.shell.querySelector('[data-jio-report-add]'), null);
assert.equal(modal.shell.querySelector('[data-jio-report-images]'), null);
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]'), null);
});
test('counts in-flight images once, accepts exactly three, and blocks submit until ready', async () => {
const runtime = createRuntime();
const modal = runtime.openReport();
selectReason(modal, 0);
const fileInput = modal.shell.querySelector('[data-aksrt-report-file]');
const fileInput = modal.shell.querySelector('[data-jio-report-file]');
fileInput.files = [1, 2, 3].map((index) => ({ name: `evidence-${index}.png`, type: 'image/png' }));
fileInput.dispatch('change');
assert.equal(callsFor(runtime, '/report/upload-image').length, 3);
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]').textContent, '3 / 3');
assert.equal(modal.shell.querySelector('[data-aksrt-report-submit]').disabled, true);
assert.equal(modal.shell.querySelector('[data-aksrt-report-status]').textContent, '图片正在上传,完成后即可提交');
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]').textContent, '3 / 3');
assert.equal(modal.shell.querySelector('[data-jio-report-submit]').disabled, true);
assert.equal(modal.shell.querySelector('[data-jio-report-status]').textContent, '图片正在上传,完成后即可提交');
submit(modal);
assert.equal(callsFor(runtime, '/report/submit').length, 0);
@@ -334,9 +334,9 @@ test('counts in-flight images once, accepts exactly three, and blocks submit unt
data: { id: 800 + index, url: `https://evidence.test/${index + 1}.png` },
}));
await flushPromises();
assert.equal(modal.shell.querySelectorAll('.aksrt-report-image').length, 3);
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]').textContent, '3 / 3');
assert.equal(modal.shell.querySelector('[data-aksrt-report-add]').disabled, true);
assert.equal(modal.shell.querySelectorAll('.jio-report-image').length, 3);
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]').textContent, '3 / 3');
assert.equal(modal.shell.querySelector('[data-jio-report-add]').disabled, true);
fileInput.files = [{ name: 'fourth.png', type: 'image/png' }];
fileInput.dispatch('change');
@@ -353,39 +353,39 @@ test('counts in-flight images once, accepts exactly three, and blocks submit unt
test('retries a failed upload and deletes its pending attachment when removed', async () => {
const runtime = createRuntime();
const modal = runtime.openReport();
const fileInput = modal.shell.querySelector('[data-aksrt-report-file]');
const fileInput = modal.shell.querySelector('[data-jio-report-file]');
fileInput.files = [{ name: 'retry.png', type: 'image/png' }];
fileInput.dispatch('change');
callsFor(runtime, '/report/upload-image')[0].resolve({ success: false, message: 'Upload failed' });
await flushPromises();
const image = modal.shell.querySelector('.aksrt-report-image');
const image = modal.shell.querySelector('.jio-report-image');
assert.equal(image.getAttribute('data-upload-state'), 'error');
assert.match(modal.shell.querySelector('[data-aksrt-report-error]').textContent, /Upload failed/);
assert.match(modal.shell.querySelector('[data-jio-report-error]').textContent, /Upload failed/);
image.querySelector('[data-aksrt-report-retry]').dispatch('click');
image.querySelector('[data-jio-report-retry]').dispatch('click');
const retry = callsFor(runtime, '/report/upload-image')[1];
assert.ok(retry);
retry.resolve({ success: true, data: { id: 901, url: 'https://evidence.test/retry.png' } });
await flushPromises();
assert.equal(image.getAttribute('data-upload-state'), 'ready');
image.querySelector('[data-aksrt-report-remove]').dispatch('click');
image.querySelector('[data-jio-report-remove]').dispatch('click');
const deletion = callsFor(runtime, '/upload/pending/901')[0];
assert.ok(deletion);
assert.equal(deletion.options.method, 'DELETE');
assert.equal(modal.shell.querySelectorAll('.aksrt-report-image').length, 0);
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]').textContent, '0 / 3');
assert.equal(modal.shell.querySelectorAll('.jio-report-image').length, 0);
assert.equal(modal.shell.querySelector('[data-jio-report-image-count]').textContent, '0 / 3');
});
test('server failure preserves values, reenables controls, and allows a retry', async () => {
const runtime = createRuntime();
const modal = runtime.openReport();
selectReason(modal, 'other');
const form = modal.shell.querySelector('.aksrt-report-form');
const form = modal.shell.querySelector('.jio-report-form');
const otherInput = form.querySelector('[name="reason_other"]');
const description = form.querySelector('[name="report_desc"]');
const submitButton = form.querySelector('[data-aksrt-report-submit]');
const submitButton = form.querySelector('[data-jio-report-submit]');
otherInput.value = 'A custom report reason';
description.value = 'Additional context';
@@ -402,7 +402,7 @@ test('server failure preserves values, reenables controls, and allows a retry',
assert.equal(otherInput.disabled, false);
assert.equal(description.disabled, false);
assert.equal(submitButton.disabled, false);
assert.equal(form.querySelector('[data-aksrt-report-error]').hidden, true);
assert.equal(form.querySelector('[data-jio-report-error]').hidden, true);
assert.deepEqual(runtime.notyfCalls.at(-1).slice(0, 3), ['Server rejected report', 'danger', 5]);
submit(modal);
@@ -416,9 +416,9 @@ test('uses a persistent shared toast per submit attempt without inline submittin
const runtime = createRuntime();
const modal = runtime.openReport();
selectReason(modal, 0);
const form = modal.shell.querySelector('.aksrt-report-form');
const button = form.querySelector('[data-aksrt-report-submit]');
const status = form.querySelector('[data-aksrt-report-status]');
const form = modal.shell.querySelector('.jio-report-form');
const button = form.querySelector('[data-jio-report-submit]');
const status = form.querySelector('[data-jio-report-status]');
submit(modal);
const firstToast = runtime.notyfCalls.at(-1);
@@ -467,7 +467,7 @@ test('blocks duplicate submits and closing an old submitting dialog cannot close
test('cleans pending attachments on close and when an upload completes after close', async () => {
const runtime = createRuntime();
const readyModal = runtime.openReport();
const readyFile = readyModal.shell.querySelector('[data-aksrt-report-file]');
const readyFile = readyModal.shell.querySelector('[data-jio-report-file]');
readyFile.files = [{ name: 'ready.png', type: 'image/png' }];
readyFile.dispatch('change');
callsFor(runtime, '/report/upload-image')[0].resolve({
@@ -482,7 +482,7 @@ test('cleans pending attachments on close and when an upload completes after clo
assert.equal(readyDeletion.options.method, 'DELETE');
const pendingModal = runtime.openReport();
const pendingFile = pendingModal.shell.querySelector('[data-aksrt-report-file]');
const pendingFile = pendingModal.shell.querySelector('[data-jio-report-file]');
pendingFile.files = [{ name: 'pending.png', type: 'image/png' }];
pendingFile.dispatch('change');
const pendingUpload = callsFor(runtime, '/report/upload-image')[1];
@@ -505,16 +505,16 @@ test('keeps a live child modal open and marks the finished report after success'
selectReason(report, 0);
submit(report);
const request = callsFor(runtime, '/report/submit')[0];
const child = runtime.modalRuntime.open({ className: 'aksrt-child-modal', html: '<p>Child dialog</p>' });
const child = runtime.modalRuntime.open({ className: 'jio-child-modal', html: '<p>Child dialog</p>' });
request.resolve({ success: true });
await flushPromises();
const form = report.shell.querySelector('.aksrt-report-form');
const form = report.shell.querySelector('.jio-report-form');
assert.equal(runtime.activeModal, child);
assert.equal(runtime.closeCalls.length, 0);
assert.equal(form.getAttribute('aria-busy'), 'false');
assert.equal(form.querySelector('[data-aksrt-report-submit]').textContent, '举报已提交');
assert.ok(report.shell.querySelector('[data-aksrt-report-status]'));
assert.equal(form.querySelector('[data-jio-report-submit]').textContent, '举报已提交');
assert.ok(report.shell.querySelector('[data-jio-report-status]'));
});
test('retains claimed evidence on post-close success and deletes it on post-close failure', async () => {
@@ -522,7 +522,7 @@ test('retains claimed evidence on post-close success and deletes it on post-clos
async function submitClosedReport(id, result) {
const modal = runtime.openReport();
selectReason(modal, 0);
const fileInput = modal.shell.querySelector('[data-aksrt-report-file]');
const fileInput = modal.shell.querySelector('[data-jio-report-file]');
fileInput.files = [{ name: `${id}.png`, type: 'image/png' }];
fileInput.dispatch('change');
callsFor(runtime, '/report/upload-image').at(-1).resolve({
+29 -29
View File
@@ -2,16 +2,16 @@
/**
* Exercise report submission, storage, notifications, and admin filtering without WordPress.
*
* @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\Settings {
namespace Jio\Settings {
final class Settings {
public static array $values = [];
public static function get_all(): array { return self::$values; }
@@ -22,24 +22,24 @@ namespace AKSRT\Settings {
}
}
namespace AKSRT\Messages {
namespace Jio\Messages {
final class Message {
public static function get_instance(): self { return new self(); }
public function send(int $user_id, string $type, string $content, array $args = []): void {
$GLOBALS['aksrt_test_messages'][] = compact('user_id', 'type', 'content', 'args');
$GLOBALS['jio_test_messages'][] = compact('user_id', 'type', 'content', 'args');
}
}
}
namespace AKSRT\Attachment {
namespace Jio\Attachment {
final class PendingUploads {
public static function claim_text(string $text, int $user_id): void {
$GLOBALS['aksrt_test_claims'][] = [$text, $user_id];
$GLOBALS['jio_test_claims'][] = [$text, $user_id];
}
}
}
namespace AKSRT\User {
namespace Jio\User {
final class Restriction {
public const TYPE_WARN = 'warn';
public const TYPE_MUTE = 'mute';
@@ -193,7 +193,7 @@ namespace {
}
final class ReportFixtureController {
use \AKSRT\Admin\UserControllerReportOperations;
use \Jio\Admin\UserControllerReportOperations;
protected function success($data = [], int $status = 200): WP_REST_Response {
return new WP_REST_Response(array_merge(['success' => true], $data), $status);
@@ -226,7 +226,7 @@ namespace {
function wp_kses_post($value): string { return strip_tags((string) $value); }
function current_time(string $type): string { unset($type); return '2026-10-30 12:00:00'; }
function wp_json_encode($value): string { return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '[]'; }
function get_current_user_id(): int { return (int) ($GLOBALS['aksrt_test_user_id'] ?? 0); }
function get_current_user_id(): int { return (int) ($GLOBALS['jio_test_user_id'] ?? 0); }
function get_userdata(int $user_id) { return $user_id > 0 ? new WP_User($user_id, 'Reporter ' . $user_id) : false; }
function get_users(array $args = []): array { unset($args); return [901]; }
function admin_url(string $path = ''): string { return 'https://example.test/wp-admin/' . ltrim($path, '/'); }
@@ -234,8 +234,8 @@ namespace {
function get_the_title($post): string { return 'Article #' . (is_object($post) ? $post->ID : (int) $post); }
function get_permalink($post): string { return 'https://example.test/post/' . (is_object($post) ? $post->ID : (int) $post); }
function do_action(string $hook, ...$args): void {
if ($hook === 'aksrt_report_submitted') {
$GLOBALS['aksrt_test_actions'][] = $args;
if ($hook === 'jio_report_submitted') {
$GLOBALS['jio_test_actions'][] = $args;
}
}
if (!function_exists('mb_substr')) {
@@ -246,12 +246,12 @@ namespace {
function reset_report_fixture(array $preset_names = ['垃圾信息', '人身攻击']): void {
$GLOBALS['wpdb'] = new TestWpdb();
$GLOBALS['aksrt_test_user_id'] = 42;
$GLOBALS['aksrt_test_actions'] = [];
$GLOBALS['aksrt_test_messages'] = [];
$GLOBALS['aksrt_test_claims'] = [];
\AKSRT\User\Restriction::$calls = [];
\AKSRT\Settings\Settings::$values = [
$GLOBALS['jio_test_user_id'] = 42;
$GLOBALS['jio_test_actions'] = [];
$GLOBALS['jio_test_messages'] = [];
$GLOBALS['jio_test_claims'] = [];
\Jio\User\Restriction::$calls = [];
\Jio\Settings\Settings::$values = [
'ban_preset_reason' => array_map(static fn (string $name): array => ['t' => $name], $preset_names),
'user_report_img_s' => true,
];
@@ -268,16 +268,16 @@ namespace {
'status' => $response->get_status(),
'rows' => $wpdb->rows,
'insert_calls' => $wpdb->insert_calls,
'actions' => $GLOBALS['aksrt_test_actions'],
'messages' => $GLOBALS['aksrt_test_messages'],
'claims' => $GLOBALS['aksrt_test_claims'],
'actions' => $GLOBALS['jio_test_actions'],
'messages' => $GLOBALS['jio_test_messages'],
'claims' => $GLOBALS['jio_test_claims'],
];
}
$results = [];
reset_report_fixture(['垃圾信息', '人身攻击']);
$results['runtime_reasons'] = \AKSRT\Settings\UserRuntime::ban_settings()['reasons'];
$results['runtime_reasons'] = \Jio\Settings\UserRuntime::ban_settings()['reasons'];
reset_report_fixture();
$results['preset_with_evidence'] = snapshot_response(submit_report_case([
@@ -349,7 +349,7 @@ namespace {
]));
reset_report_fixture();
$GLOBALS['aksrt_test_user_id'] = 0;
$GLOBALS['jio_test_user_id'] = 0;
$results['unauthenticated'] = snapshot_response(submit_report_case([
'target_type' => 'post', 'target_id' => 5, 'category' => '垃圾信息', 'reason' => '',
]));
@@ -400,7 +400,7 @@ namespace {
];
reset_report_fixture();
$service_id = \AKSRT\Messages\ReportCenter::get_instance()->submit(42, 'post', 6, '详情', '');
$service_id = \Jio\Messages\ReportCenter::get_instance()->submit(42, 'post', 6, '详情', '');
$results['service_blank_category'] = [
'id' => $service_id,
'rows' => $GLOBALS['wpdb']->rows,
@@ -415,9 +415,9 @@ namespace {
'target_type' => 'post', 'target_id' => 62, 'category' => '待删除的分类', 'reason' => '',
]);
$admin_controller = new ReportFixtureController();
\AKSRT\Settings\Settings::$values['ban_preset_reason'] = [['t' => '当前新分类']];
\Jio\Settings\Settings::$values['ban_preset_reason'] = [['t' => '当前新分类']];
$changed_list = $admin_controller->get_report_list(new WP_REST_Request())->get_data()['data'] ?? [];
\AKSRT\Settings\Settings::$values['ban_preset_reason'] = [];
\Jio\Settings\Settings::$values['ban_preset_reason'] = [];
$removed_list = $admin_controller->get_report_list(new WP_REST_Request())->get_data()['data'] ?? [];
$first_detail = $admin_controller->get_report_detail(new WP_REST_Request(['id' => 1]))->get_data()['data']['report'] ?? [];
$second_detail = $admin_controller->get_report_detail(new WP_REST_Request(['id' => 2]))->get_data()['data']['report'] ?? [];
@@ -444,7 +444,7 @@ namespace {
$preset_row = $GLOBALS['wpdb']->rows[0];
$results['process_preset_only'] = [
'success' => !empty($preset_process->get_data()['success']),
'restriction' => \AKSRT\User\Restriction::$calls[0] ?? [],
'restriction' => \Jio\User\Restriction::$calls[0] ?? [],
'report_reason' => $preset_row['reason'],
'report_category' => $preset_row['category'],
];
@@ -462,7 +462,7 @@ namespace {
$other_row = $GLOBALS['wpdb']->rows[0];
$results['process_other_with_details'] = [
'success' => !empty($other_process->get_data()['success']),
'restriction' => \AKSRT\User\Restriction::$calls[0] ?? [],
'restriction' => \Jio\User\Restriction::$calls[0] ?? [],
'report_reason' => $other_row['reason'],
'report_category' => $other_row['category'],
];
@@ -134,12 +134,12 @@ test('preserves readable labels through the admin category filter, list, and det
assert.equal(result.detail_report.category, '涉嫌侵权');
});
test('stores report categories as text and uses schema version 3.2.0', () => {
test('stores report categories as text and uses schema version 3.3.0', () => {
const migrator = fs.readFileSync(path.join(root, 'src/Core/DBMigrator.php'), 'utf8');
const tableDefinitions = fs.readFileSync(path.join(root, 'src/Core/DBMigratorPart2.php'), 'utf8');
const reportTable = tableDefinitions.match(/\$sql_report\s*=\s*"([\s\S]*?)";/);
assert.match(migrator, /const SCHEMA_VERSION\s*=\s*'3\.2\.0';/);
assert.match(migrator, /const SCHEMA_VERSION\s*=\s*'3\.3\.0';/);
assert.ok(reportTable, 'report table definition should exist');
assert.match(reportTable[1], /\bcategory\s+TEXT\s+NOT NULL\b/);
});
+51 -51
View File
@@ -4,19 +4,19 @@ declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
use AKSRT\WidgetSystem\FieldSchema;
use AKSRT\WidgetSystem\FormRenderer;
use AKSRT\WidgetSystem\PostQuery;
use AKSRT\WidgetSystem\RestController;
use AKSRT\WidgetSystem\SchemaWidget;
use AKSRT\WidgetSystem\WidgetDefinition;
use AKSRT\WidgetSystem\WidgetRegistry;
use AKSRT\WidgetSystem\WidgetTransfer;
use AKSRT\WidgetSystem\WidgetVisibility;
use AKSRT\WidgetSystem\WidgetSchemas;
use AKSRT\WidgetSystem\Renderers\ContentRenderer;
use AKSRT\WidgetSystem\Renderers\GeneralRenderer;
use AKSRT\WidgetSystem\Renderers\SliderRenderer;
use Jio\WidgetSystem\FieldSchema;
use Jio\WidgetSystem\FormRenderer;
use Jio\WidgetSystem\PostQuery;
use Jio\WidgetSystem\RestController;
use Jio\WidgetSystem\SchemaWidget;
use Jio\WidgetSystem\WidgetDefinition;
use Jio\WidgetSystem\WidgetRegistry;
use Jio\WidgetSystem\WidgetTransfer;
use Jio\WidgetSystem\WidgetVisibility;
use Jio\WidgetSystem\WidgetSchemas;
use Jio\WidgetSystem\Renderers\ContentRenderer;
use Jio\WidgetSystem\Renderers\GeneralRenderer;
use Jio\WidgetSystem\Renderers\SliderRenderer;
if (!function_exists('current_datetime')) {
function current_datetime(): DateTimeImmutable {
@@ -114,7 +114,7 @@ final class WidgetSystemTest {
$this->same('1,-2,3', $reference['cat'], 'reference cat preserves cleaned signed IDs for WP_Query descendant semantics');
$this->false(isset($reference['category__in']) || isset($reference['category__not_in']), 'reference cat does not narrow matches to direct categories');
$this->false(isset($reference['tax_query']), 'topic filters are removed with the topics feature');
$this->same('aksrt_post_views', $reference['meta_key'], 'reference view ordering uses numeric view meta');
$this->same('jio_post_views', $reference['meta_key'], 'reference view ordering uses numeric view meta');
$this->false(isset($reference['meta_query']), 'modern reference queries do not apply commerce filters by default');
$this->same(1, $reference['paged'], 'no-pagination queries stay on first page');
$this->same('2026-06-08 12:30:00', $reference['date_query'][0]['after'], 'limit_day subtracts days in the WordPress site timezone');
@@ -260,31 +260,31 @@ final class WidgetSystemTest {
$this->false(WidgetVisibility::should_render(['show_id_type' => 'show', 'show_ids' => '3,8'], 0, false), 'show list rejects archives');
$this->false(WidgetVisibility::should_render(['show_id_type' => 'hide', 'show_ids' => '3,8'], 3, true), 'hide list suppresses matching ID');
$this->true(WidgetVisibility::should_render(['show_id_type' => 'hide', 'show_ids' => '3,8'], 0, false), 'hide list keeps archives');
$this->same('aksrt-widget--only-pc', WidgetVisibility::device_class(['show_type' => 'only_pc']), 'PC device class is stable');
$this->same('aksrt-widget--only-mobile', WidgetVisibility::device_class(['show_type' => 'only_sm']), 'mobile device class is stable');
$this->same('jio-widget--only-pc', WidgetVisibility::device_class(['show_type' => 'only_pc']), 'PC device class is stable');
$this->same('jio-widget--only-mobile', WidgetVisibility::device_class(['show_type' => 'only_sm']), 'mobile device class is stable');
$this->same('', WidgetVisibility::device_class(['show_type' => 'invalid']), 'invalid device rule adds no class');
}
private function transfer_round_trip(): void {
aksrt_test_reset_state();
jio_test_reset_state();
$GLOBALS['wp_registered_sidebars'] = ['home_sidebar' => [], 'single_sidebar' => []];
update_option('widget_aksrt_widget_posts', [2 => ['title' => '<b>课程列表</b>', 'count' => 999, 'style' => 'mini'], '_multiwidget' => 1]);
$GLOBALS['aksrt_test_state']['sidebars'] = ['home_sidebar' => ['text-1', 'aksrt_widget_posts-2']];
update_option('widget_jio_widget_posts', [2 => ['title' => '<b>课程列表</b>', 'count' => 999, 'style' => 'mini'], '_multiwidget' => 1]);
$GLOBALS['jio_test_state']['sidebars'] = ['home_sidebar' => ['text-1', 'jio_widget_posts-2']];
$payload = WidgetTransfer::export('home_sidebar');
$this->same(WidgetTransfer::FORMAT, $payload['format'], 'export format is versioned');
$this->same(1, count($payload['sidebars']['home_sidebar']), 'export ignores non-schema widgets');
$this->same(20, $payload['sidebars']['home_sidebar'][0]['settings']['count'], 'export normalizes saved settings');
update_option('widget_aksrt_widget_posts', []);
$GLOBALS['aksrt_test_state']['sidebars']['single_sidebar'] = ['text-9'];
update_option('widget_jio_widget_posts', []);
$GLOBALS['jio_test_state']['sidebars']['single_sidebar'] = ['text-9'];
$result = WidgetTransfer::import($payload, 'append', 'single_sidebar');
$this->same(['sidebars' => 1, 'widgets' => 1], $result, 'append import reports changes');
$this->same(['text-9', 'aksrt_widget_posts-1'], $GLOBALS['aksrt_test_state']['sidebars']['single_sidebar'], 'append import preserves existing widgets');
$this->same(['text-9', 'jio_widget_posts-1'], $GLOBALS['jio_test_state']['sidebars']['single_sidebar'], 'append import preserves existing widgets');
$round_trip = WidgetTransfer::export('single_sidebar');
$this->same($payload['sidebars']['home_sidebar'][0], $round_trip['sidebars']['single_sidebar'][0], 'export/import round trip preserves normalized module');
$replacement = WidgetTransfer::template_payload('links_navigation');
$result = WidgetTransfer::import($replacement, 'replace', 'single_sidebar');
$this->same(3, $result['widgets'], 'targeted template import combines template areas');
$this->false(in_array('text-9', $GLOBALS['aksrt_test_state']['sidebars']['single_sidebar'], true), 'replace import removes previous assignment');
$this->false(in_array('text-9', $GLOBALS['jio_test_state']['sidebars']['single_sidebar'], true), 'replace import removes previous assignment');
try {
WidgetTransfer::import(['format' => 'legacy.widgets', 'sidebars' => []]);
$this->failures[] = 'transfer rejects legacy formats: no exception thrown';
@@ -294,45 +294,45 @@ final class WidgetSystemTest {
}
private function rest_contract(): void {
aksrt_test_reset_state();
jio_test_reset_state();
$controller = new RestController();
$controller->register_routes();
$this->same(1, count($GLOBALS['aksrt_test_state']['routes']), 'REST registers only the widget items route');
$items_route = 'aksrt/v1/widgets/(?P<widget>[a-z][a-z0-9_]*)/items';
$route = $GLOBALS['aksrt_test_state']['routes'][$items_route] ?? [];
$this->true(isset($GLOBALS['aksrt_test_state']['routes'][$items_route]), 'items route remains registered');
$this->false(isset($GLOBALS['aksrt_test_state']['routes']['aksrt/v1/widgets/quote']), 'obsolete quote REST route is removed');
$this->same(1, count($GLOBALS['jio_test_state']['routes']), 'REST registers only the widget items route');
$items_route = 'jio/v1/widgets/(?P<widget>[a-z][a-z0-9_]*)/items';
$route = $GLOBALS['jio_test_state']['routes'][$items_route] ?? [];
$this->true(isset($GLOBALS['jio_test_state']['routes'][$items_route]), 'items route remains registered');
$this->false(isset($GLOBALS['jio_test_state']['routes']['jio/v1/widgets/quote']), 'obsolete quote REST route is removed');
$this->false(method_exists($controller, 'get_quote'), 'obsolete quote REST callback is removed');
$this->same('__return_true', $route['permission_callback'], 'public items route is read-only accessible');
$this->true($controller->validate_widget('posts'), 'known widget validates');
$this->false($controller->validate_widget('shop_products'), 'unknown/shop widget is rejected');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 1, 'query' => 'arbitrary']));
$this->error('aksrt_widget_unknown_parameter', $error, 'REST rejects arbitrary query parameters');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'search', 'instance' => 'aksrt_widget_search-1', 'page' => 1]));
$this->error('aksrt_widget_items_unsupported', $error, 'REST rejects non-dynamic widgets');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 1]));
$this->error('aksrt_widget_instance_not_registered', $error, 'REST requires an assigned instance');
$GLOBALS['aksrt_test_state']['sidebars'] = ['home_top_content' => ['aksrt_widget_posts-1']];
update_option('widget_aksrt_widget_posts', [1 => ['title' => 'Saved only', 'count' => 2, 'style' => 'list', 'paginate' => 'ajax'], '_multiwidget' => 1]);
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 2, 'segment' => '']));
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'jio_widget_posts-1', 'page' => 1, 'query' => 'arbitrary']));
$this->error('jio_widget_unknown_parameter', $error, 'REST rejects arbitrary query parameters');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'search', 'instance' => 'jio_widget_search-1', 'page' => 1]));
$this->error('jio_widget_items_unsupported', $error, 'REST rejects non-dynamic widgets');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'jio_widget_posts-1', 'page' => 1]));
$this->error('jio_widget_instance_not_registered', $error, 'REST requires an assigned instance');
$GLOBALS['jio_test_state']['sidebars'] = ['home_top_content' => ['jio_widget_posts-1']];
update_option('widget_jio_widget_posts', [1 => ['title' => 'Saved only', 'count' => 2, 'style' => 'list', 'paginate' => 'ajax'], '_multiwidget' => 1]);
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'jio_widget_posts-1', 'page' => 2, 'segment' => '']));
$this->true($response instanceof WP_REST_Response, 'registered instance returns REST response');
$data = $response->get_data();
$this->true($data['success'], 'REST success envelope is stable');
$this->same(2, $data['page'], 'REST page reaches saved-query pipeline');
$this->contains('暂无内容', $data['html'], 'REST returns deterministic empty state');
$this->same(4, end($GLOBALS['aksrt_test_state']['query_args'])['posts_per_page'], 'REST reloads saved normalized configuration');
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 1, 'rest_route' => '/aksrt/v1/widgets/posts/items']));
$this->same(4, end($GLOBALS['jio_test_state']['query_args'])['posts_per_page'], 'REST reloads saved normalized configuration');
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'jio_widget_posts-1', 'page' => 1, 'rest_route' => '/jio/v1/widgets/posts/items']));
$this->true($response instanceof WP_REST_Response, 'plain permalink transport parameter is accepted');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_hot_posts-1', 'page' => 1]));
$this->error('aksrt_widget_invalid_instance', $error, 'REST binds instance ID to requested widget');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'jio_widget_hot_posts-1', 'page' => 1]));
$this->error('jio_widget_invalid_instance', $error, 'REST binds instance ID to requested widget');
}
private function payment_states(): void {
aksrt_test_reset_state();
$GLOBALS['aksrt_test_state']['current_post'] = new WP_Post(77, 5);
jio_test_reset_state();
$GLOBALS['jio_test_state']['current_post'] = new WP_Post(77, 5);
$method = new ReflectionMethod(ContentRenderer::class, 'post_pay');
$render = static fn(): string => (string) $method->invoke(null, ['theme' => 'jb-red']);
$GLOBALS['aksrt_test_state']['pay_meta'] = ['pay_type' => 'no', 'price' => 0];
$GLOBALS['jio_test_state']['pay_meta'] = ['pay_type' => 'no', 'price' => 0];
$this->same('', $render(), 'unconfigured article renders nothing and aborts the widget');
$authorized = [
@@ -341,15 +341,15 @@ final class WidgetSystemTest {
'vip' => ['pay_type' => 'vip', 'price' => 0],
];
foreach ($authorized as $label => $meta) {
$GLOBALS['aksrt_test_state']['pay_meta'] = $meta;
$GLOBALS['aksrt_test_state']['paid_access'] = true;
$GLOBALS['jio_test_state']['pay_meta'] = $meta;
$GLOBALS['jio_test_state']['paid_access'] = true;
$this->contains('内容已解锁', $render(), $label . ' authorization delegates to existing access service');
}
$GLOBALS['aksrt_test_state']['pay_meta'] = ['pay_type' => 'video', 'price' => 49];
$GLOBALS['aksrt_test_state']['paid_access'] = false;
$GLOBALS['aksrt_test_state']['logged_in'] = false;
$GLOBALS['jio_test_state']['pay_meta'] = ['pay_type' => 'video', 'price' => 49];
$GLOBALS['jio_test_state']['paid_access'] = false;
$GLOBALS['jio_test_state']['logged_in'] = false;
$this->contains('guest-locked', $render(), 'guest unauthorized state delegates to paywall');
$GLOBALS['aksrt_test_state']['logged_in'] = true;
$GLOBALS['jio_test_state']['logged_in'] = true;
$this->contains('member-locked', $render(), 'logged-in unauthorized state delegates to paywall');
}
@@ -3,10 +3,10 @@
declare(strict_types=1);
define('ABSPATH', __DIR__ . DIRECTORY_SEPARATOR);
define('AKSRT_VERSION', 'theme-fallback');
define('AKSRT_URI', 'https://example.test/theme');
define('JIO_VERSION', 'theme-fallback');
define('JIO_URI', 'https://example.test/theme');
$GLOBALS['runtime_asset_fixture_root'] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'aksrt-runtime-assets-' . bin2hex(random_bytes(6));
$GLOBALS['runtime_asset_fixture_root'] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'jio-runtime-assets-' . bin2hex(random_bytes(6));
$GLOBALS['runtime_asset_localized'] = [];
mkdir($GLOBALS['runtime_asset_fixture_root'] . '/assets/js/runtime', 0777, true);
@@ -36,17 +36,17 @@ function is_user_logged_in(): bool { return false; }
function get_current_user_id(): int { return 0; }
function wp_max_upload_size(): int { return 1048576; }
function is_page_template(string $template = ''): bool { return false; }
function aksrt_is_lazy(string $scope): bool { return false; }
function aksrt_get_lazy_thumb(string $size = ''): string { return 'https://example.test/lazy.svg'; }
function aksrt_get_default_avatar_url(): string { return 'https://example.test/avatar.png'; }
function jio_is_lazy(string $scope): bool { return false; }
function jio_get_lazy_thumb(string $size = ''): string { return 'https://example.test/lazy.svg'; }
function jio_get_default_avatar_url(): string { return 'https://example.test/avatar.png'; }
require dirname(__DIR__, 2) . '/src/Core/ScriptsPart1c.php';
class RuntimeAssetBehaviorHarness {
use \AKSRT\Core\ScriptsPart1c;
use \Jio\Core\ScriptsPart1c;
public function enqueue(): void {
$this->enqueue_public_fonts_js_and_data('https://example.test/theme', 'aksrt-public', [], false, false, false, false, AKSRT_VERSION, []);
$this->enqueue_public_fonts_js_and_data('https://example.test/theme', 'jio-public', [], false, false, false, false, JIO_VERSION, []);
}
private function resolve_frontend_routes(): array {
@@ -93,5 +93,5 @@ rmdir(get_template_directory());
echo json_encode([
'versions' => $versions,
'expected' => [(string) ($baseTime + 30), (string) ($baseTime + 40), (string) ($baseTime + 50), AKSRT_VERSION],
'expected' => [(string) ($baseTime + 30), (string) ($baseTime + 40), (string) ($baseTime + 50), JIO_VERSION],
], JSON_THROW_ON_ERROR);
@@ -6,15 +6,15 @@ const path = require('node:path')
const root = path.resolve(__dirname, '../..')
const read = file => fs.readFileSync(path.join(root, file), 'utf8')
const pay = read('inc/helpers/aksrt-pay.php')
const resource = read('inc/helpers/aksrt-resource.php')
const pay = read('inc/helpers/jio-pay.php')
const resource = read('inc/helpers/jio-resource.php')
const list = read('src/Admin/Native/PostListEnhance.php')
const domain = read('src/Core/DomainModel.php')
test('sales count has a single read path that adds the 销量浮动', () => {
// 展示销量只有一个口径:真实订单 + 后台「销量浮动」(pay_count)。
// 此前各调用点各自拼 virtual_sales / sales_volume,pay_count 写进去却没人读。
assert.match(pay, /function aksrt_get_content_sales_count\(/)
assert.match(pay, /function jio_get_content_sales_count\(/)
assert.match(pay, /RESOURCE_SALES_VOLUME_KEY/)
assert.match(pay, /\$pay_meta\['pay_count'\]/)
// 两部分都必须下限为 0,避免负的浮动把销量拉成负数。
@@ -23,9 +23,9 @@ test('sales count has a single read path that adds the 销量浮动', () => {
test('every sales_count producer goes through that read path', () => {
// 内容帖、资源帖、付费墙、付费盒四处不得再各自拼装销量。
assert.match(pay, /'sales_count'\s*=>\s*\$show_sales \? aksrt_get_content_sales_count\(\$p, \$pay_meta\) : 0/)
assert.match(resource, /'sales_count'\s*=>\s*\$show_sales \? aksrt_get_content_sales_count\(\$p, \$pay_meta\) : 0/)
assert.match(pay, /\$sales = aksrt_get_content_sales_count\(\$post, \$meta\)/)
assert.match(pay, /'sales_count'\s*=>\s*\$show_sales \? jio_get_content_sales_count\(\$p, \$pay_meta\) : 0/)
assert.match(resource, /'sales_count'\s*=>\s*\$show_sales \? jio_get_content_sales_count\(\$p, \$pay_meta\) : 0/)
assert.match(pay, /\$sales = jio_get_content_sales_count\(\$post, \$meta\)/)
// 不得再出现按 virtual_sales 现拼销量的旧写法。
assert.doesNotMatch(pay, /\$sales\s*=\s*max\(0, \(int\) \(\$meta\['virtual_sales'\] \?\? 0\)\);/)
@@ -73,31 +73,31 @@ class wpdb {
$GLOBALS['wpdb'] = new wpdb();
// Fake posts the stub WP_Query will serve, keyed by expected cat.
$GLOBALS['aksrt_harness_posts'] = [
$GLOBALS['jio_harness_posts'] = [
['id' => 101, 'title' => '第一篇文章', 'cat' => 5],
['id' => 102, 'title' => '第二篇文章', 'cat' => 5],
['id' => 103, 'title' => '第三篇文章', 'cat' => 5],
['id' => 104, 'title' => '第四篇文章', 'cat' => 5],
];
$GLOBALS['aksrt_harness_cursor'] = 0;
$GLOBALS['jio_harness_cursor'] = 0;
class WP_Query {
public array $posts = [];
public int $found_posts = 0;
public function __construct(public array $query_args = []) {
$cat_in = (array) ($query_args['category__in'] ?? []);
$matched = array_values(array_filter($GLOBALS['aksrt_harness_posts'], static fn(array $p): bool => in_array($p['cat'], $cat_in, true)));
$matched = array_values(array_filter($GLOBALS['jio_harness_posts'], static fn(array $p): bool => in_array($p['cat'], $cat_in, true)));
$this->found_posts = count($matched);
foreach ($matched as $post) {
$this->posts[] = new WP_Post($post['id']);
$GLOBALS['aksrt_harness_lookup'][$post['id']] = $post;
$GLOBALS['jio_harness_lookup'][$post['id']] = $post;
}
}
public function have_posts(): bool { return $GLOBALS['aksrt_harness_cursor'] < count($this->posts); }
public function have_posts(): bool { return $GLOBALS['jio_harness_cursor'] < count($this->posts); }
public function the_post(): void {
$post = $this->posts[$GLOBALS['aksrt_harness_cursor']];
$GLOBALS['aksrt_harness_cursor']++;
$GLOBALS['aksrt_harness_current_post'] = $GLOBALS['aksrt_harness_lookup'][$post->ID] ?? ['id' => $post->ID, 'title' => '文章' . $post->ID];
$post = $this->posts[$GLOBALS['jio_harness_cursor']];
$GLOBALS['jio_harness_cursor']++;
$GLOBALS['jio_harness_current_post'] = $GLOBALS['jio_harness_lookup'][$post->ID] ?? ['id' => $post->ID, 'title' => '文章' . $post->ID];
}
}
@@ -141,7 +141,7 @@ function human_time_diff(int $from, int $to = 0): string {
function get_avatar_url($id, array $args = []): string { return 'https://example.test/avatar-' . (int) $id . '.jpg'; }
function get_post_meta($id, $key = '', $single = false) { unset($id, $key, $single); return ''; }
function get_term_meta(int $term_id, string $key, bool $single = false) {
if ($key === 'aksrt_category_cover_id') {
if ($key === 'jio_category_cover_id') {
return $term_id === 5 ? 55 : 0;
}
return '';
@@ -170,10 +170,10 @@ function get_term(int $term_id, string $taxonomy = '') {
function get_term_link(WP_Term $term) {
return 'https://example.test/cat-' . $term->term_id;
}
function get_the_ID(): int { return (int) ($GLOBALS['aksrt_harness_current_post']['id'] ?? 0); }
function get_the_ID(): int { return (int) ($GLOBALS['jio_harness_current_post']['id'] ?? 0); }
function get_the_title($id = 0): string {
unset($id);
return (string) ($GLOBALS['aksrt_harness_current_post']['title'] ?? '');
return (string) ($GLOBALS['jio_harness_current_post']['title'] ?? '');
}
function get_permalink($id = 0): string { return 'https://example.test/post-' . (int) ($id ?: get_the_ID()); }
function get_the_excerpt($id = 0): string { unset($id); return '这是摘要'; }
@@ -191,19 +191,19 @@ function get_comments_link($id = 0): string { return get_permalink($id) . '#comm
function comments_open($id = 0): bool { unset($id); return true; }
function get_post_type($id = 0): string { unset($id); return 'post'; }
function has_post_format($formats, $id = 0): bool { unset($formats, $id); return false; }
function aksrt_get_pay_meta($post = null): array { unset($post); return ['pay_type' => 'no', 'price' => 0]; }
function aksrt_get_post_views($id = 0): int { return (int) ($id % 90) + 10; }
function aksrt_get_post_likes($id = 0): int { return (int) ($id % 7); }
function aksrt_get_post_favorites($id = 0): int { return (int) ($id % 5); }
function aksrt_resource_card_badge_text($id = 0): string { unset($id); return ''; }
function aksrt_format_number($num): string { return (string) $num; }
function aksrt_is_lazy(string $key, bool $default = false): bool { unset($key); return $default; }
function aksrt_get_lazy_thumb(string $size = ''): string { unset($size); return 'https://example.test/lazy.svg'; }
function aksrt_get_lazy_attr(string $type, string $src, string $class = '', string $lazy_src = ''): string {
function jio_get_pay_meta($post = null): array { unset($post); return ['pay_type' => 'no', 'price' => 0]; }
function jio_get_post_views($id = 0): int { return (int) ($id % 90) + 10; }
function jio_get_post_likes($id = 0): int { return (int) ($id % 7); }
function jio_get_post_favorites($id = 0): int { return (int) ($id % 5); }
function jio_resource_card_badge_text($id = 0): string { unset($id); return ''; }
function jio_format_number($num): string { return (string) $num; }
function jio_is_lazy(string $key, bool $default = false): bool { unset($key); return $default; }
function jio_get_lazy_thumb(string $size = ''): string { unset($size); return 'https://example.test/lazy.svg'; }
function jio_get_lazy_attr(string $type, string $src, string $class = '', string $lazy_src = ''): string {
$url = $lazy_src !== '' ? $lazy_src : $src;
return ' class="' . esc_attr($class) . '" src="' . esc_url($url) . '" data-src="' . esc_url($src) . '" data-lazy-type="' . esc_attr($type) . '"';
}
function aksrt_renderer_resolve_images($post_id, array $cfg): array {
function jio_renderer_resolve_images($post_id, array $cfg): array {
unset($post_id, $cfg);
return ['primary' => '', 'has_real' => false, 'gallery' => [], 'slides' => [], 'is_slide' => false, 'video_url' => ''];
}
@@ -241,11 +241,11 @@ class WP_Widget {
}
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/jio-graphic-card.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-widgets-markup.php';
require dirname(__DIR__, 2) . '/inc/helpers/posts-renderer/post-data.php';
use AKSRT\WidgetSystem\Renderers\ContentRenderer;
use Jio\WidgetSystem\Renderers\ContentRenderer;
$instance = [
'term_id' => [5, 8],
@@ -270,7 +270,7 @@ try {
// Single-term variant (is_row=false) + animation variant.
try {
$GLOBALS['aksrt_harness_cursor'] = 0;
$GLOBALS['jio_harness_cursor'] = 0;
$single = ContentRenderer::render('term_aggregation', array_merge($instance, ['term_id' => [5], 'obs_animation' => 'fade']));
echo "=== SINGLE OK (len=" . strlen($single) . ") ===\n";
echo $single . "\n";
@@ -280,16 +280,16 @@ try {
// Full pipeline: registry -> definition -> normalize(raw form input '5,8') -> FrontendRenderer.
try {
$definitions = \AKSRT\WidgetSystem\WidgetRegistry::all();
$definitions = \Jio\WidgetSystem\WidgetRegistry::all();
echo "=== REGISTRY: term_aggregation present = " . (isset($definitions['term_aggregation']) ? 'yes' : 'NO') . " ===\n";
$definition = $definitions['term_aggregation'];
$widget = new \AKSRT\WidgetSystem\SchemaWidget($definition);
$widget = new \Jio\WidgetSystem\SchemaWidget($definition);
$widget->_set(2);
$normalized = $definition->normalize(['term_id' => '5,8']);
echo "=== NORMALIZED term_id = " . json_encode($normalized['term_id']) . " ===\n";
$GLOBALS['aksrt_harness_cursor'] = 0;
$GLOBALS['jio_harness_cursor'] = 0;
ob_start();
\AKSRT\WidgetSystem\FrontendRenderer::render(
\Jio\WidgetSystem\FrontendRenderer::render(
$definition,
['before_widget' => '<section class="w">', 'after_widget' => '</section>'],
$normalized,
@@ -301,7 +301,7 @@ try {
// Empty selection must render nothing at all.
ob_start();
\AKSRT\WidgetSystem\FrontendRenderer::render($definition, ['before_widget' => '<section>', 'after_widget' => '</section>'], $definition->normalize([]), $widget);
\Jio\WidgetSystem\FrontendRenderer::render($definition, ['before_widget' => '<section>', 'after_widget' => '</section>'], $definition->normalize([]), $widget);
$empty = (string) ob_get_clean();
echo "=== EMPTY SELECTION OUTPUT len=" . strlen($empty) . " ===\n";
@@ -309,12 +309,12 @@ try {
ob_start();
$widget->render_full_form($definition->normalize(['term_id' => '5,8']));
$form = (string) ob_get_clean();
if (preg_match('/<div class="aksrt-widget-field aksrt-widget-field--terms".*?<\/div>\s*<\/div>\s*<\/div>/s', $form, $m)) {
if (preg_match('/<div class="jio-widget-field jio-widget-field--terms".*?<\/div>\s*<\/div>\s*<\/div>/s', $form, $m)) {
echo "=== TERMS FIELD HTML ===\n" . $m[0] . "\n";
}
echo "=== FORM contains hidden terms value name = " . (strpos($form, 'name="widget-aksrt_widget_term_aggregation[2][term_id]"') !== false ? 'yes' : 'NO') . " ===\n";
echo "=== FORM contains hidden terms value name = " . (strpos($form, 'name="widget-jio_widget_term_aggregation[2][term_id]"') !== false ? 'yes' : 'NO') . " ===\n";
echo "=== FORM hidden value = ";
preg_match('/class="aksrt-widget-terms__value"[^>]*value="([^"]*)"/', $form, $v);
preg_match('/class="jio-widget-terms__value"[^>]*value="([^"]*)"/', $form, $v);
echo ($v[1] ?? '(not found)') . " ===\n";
} catch (Throwable $e) {
echo '=== FULL PATH FATAL: ' . get_class($e) . ': ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine() . "\n";
@@ -14,7 +14,7 @@ test('search route keeps the reference Zibll results and empty-state structure',
for (const token of ['search-input', 'search-page-ajaxpager', 'search-tab-header', 'search-filter-item', 'post_ajax_loader']) {
assert.match(template + styles, new RegExp(token.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')));
}
assert.match(template, /aksrt_get_ajax_null\(__\('请输入搜索关键词'/);
assert.match(template, /jio_get_ajax_null\(__\('请输入搜索关键词'/);
assert.match(template, /'null-search\.svg'/);
assert.match(template, /user_query instanceof WP_User_Query/);
assert.equal(fs.existsSync(path.join(root, 'assets/css/routes/search.css')), false);
+37 -37
View File
@@ -37,7 +37,7 @@ function add_query_arg($key, $value = null, $url = ''): string {
}
return rtrim($url, '/') . '/?' . http_build_query($args);
}
function get_bloginfo($key): string { return $key === 'name' ? 'AKSRT' : ''; }
function get_bloginfo($key): string { return $key === 'name' ? 'Jio' : ''; }
function number_format_i18n($number): string { return number_format((float) $number); }
function wp_kses_post($html): string { return strip_tags((string) $html, '<a><b><span><i><div><p><ul><li>'); }
function is_wp_error($value): bool { return false; }
@@ -46,20 +46,20 @@ function dynamic_sidebar($id): void {}
function get_header(): void {}
function get_sidebar(): void {}
function get_footer(): void {}
function aksrt_save_history_search(string $value): void {}
function aksrt_update_search_keywords(string $value): void {}
function aksrt_search_history_enabled(): bool { return false; }
function aksrt_get_search_hot_keywords(): array { return []; }
function aksrt_get_search_type_options(): array { return ['post' => '文章', 'user' => '用户']; }
function aksrt_get_search_form_type(): string { return (string) ($_REQUEST['type'] ?? 'post'); }
function aksrt_get_search_form_state(): array {
return ['type' => aksrt_get_search_form_type(), 'types' => aksrt_get_search_type_options(), 'category_id' => 0, 'category_enabled' => false, 'categories' => []];
function jio_save_history_search(string $value): void {}
function jio_update_search_keywords(string $value): void {}
function jio_search_history_enabled(): bool { return false; }
function jio_get_search_hot_keywords(): array { return []; }
function jio_get_search_type_options(): array { return ['post' => '文章', 'user' => '用户']; }
function jio_get_search_form_type(): string { return (string) ($_REQUEST['type'] ?? 'post'); }
function jio_get_search_form_state(): array {
return ['type' => jio_get_search_form_type(), 'types' => jio_get_search_type_options(), 'category_id' => 0, 'category_enabled' => false, 'categories' => []];
}
class WP_Term {
public function __construct(public int $term_id, public string $name, public string $taxonomy = 'category') {}
}
function aksrt_get_search_categories(?array $ids = null): array {
function jio_get_search_categories(?array $ids = null): array {
return array_map(static fn($id) => new WP_Term((int) $id, (int) $id === 7 ? 'Tech' : 'News'), $ids ?? []);
}
function get_term($id) { return new WP_Term((int) $id, 'Tech'); }
@@ -78,40 +78,40 @@ class WP_User_Query {
public function get_total(): int { return $GLOBALS['user_total']; }
}
function get_userdata($id) { return $GLOBALS['user_data'][(int) $id] ?? false; }
function aksrt_get_user_home_url($id): string { return 'https://example.test/user/' . (int) $id; }
function get_author_posts_url($id): string { return aksrt_get_user_home_url($id); }
function aksrt_get_avatar_url($id, $args = []): string { return 'https://example.test/avatar/' . (int) $id . '.png'; }
function aksrt_get_lazy_attr($mode, $url, $class = '', $placeholder = ''): string {
function jio_get_user_home_url($id): string { return 'https://example.test/user/' . (int) $id; }
function get_author_posts_url($id): string { return jio_get_user_home_url($id); }
function jio_get_avatar_url($id, $args = []): string { return 'https://example.test/avatar/' . (int) $id . '.png'; }
function jio_get_lazy_attr($mode, $url, $class = '', $placeholder = ''): string {
return ' class="' . esc_attr($class) . '" src="' . esc_attr($url) . '"';
}
function aksrt_get_lazy_thumb($size): string { return ''; }
function aksrt_get_user_name($args): string {
function jio_get_lazy_thumb($size): string { return ''; }
function jio_get_user_name($args): string {
$user = get_userdata((int) $args['id']);
$name = function_exists('aksrt_renderer_highlight_text')
? aksrt_renderer_highlight_text((string) $user->display_name, (string) ($args['highlight_query'] ?? ''))
$name = function_exists('jio_renderer_highlight_text')
? jio_renderer_highlight_text((string) $user->display_name, (string) ($args['highlight_query'] ?? ''))
: esc_html($user->display_name);
return '<name class="flex ac flex1"><a class="display-name" href="' . esc_url(aksrt_get_user_home_url($user->ID)) . '">' . $name . '</a></name>';
return '<name class="flex ac flex1"><a class="display-name" href="' . esc_url(jio_get_user_home_url($user->ID)) . '">' . $name . '</a></name>';
}
function aksrt_get_user_follow($class, $id): string { return '<a class="follow flex0" data-follow="' . (int) $id . '">Follow</a>'; }
function aksrt_get_user_description($id): string { return (string) (get_userdata($id)->description ?? ''); }
function jio_get_user_follow($class, $id): string { return '<a class="follow flex0" data-follow="' . (int) $id . '">Follow</a>'; }
function jio_get_user_description($id): string { return (string) (get_userdata($id)->description ?? ''); }
function get_user_meta($id, $key, $single = true): string { return ''; }
function aksrt_get_ajax_null(?string $text = null, $margin = 60, string $img = 'null.svg', int $width = 280): string {
return '<div class="ajax-item aksrt-ajax-null" data-img="' . esc_attr($img) . '" data-width="' . $width . '">' . esc_html((string) $text) . '</div>';
function jio_get_ajax_null(?string $text = null, $margin = 60, string $img = 'null.svg', int $width = 280): string {
return '<div class="ajax-item jio-ajax-null" data-img="' . esc_attr($img) . '" data-width="' . $width . '">' . esc_html((string) $text) . '</div>';
}
function aksrt_get_null($text = null, $margin = 60, string $img = 'null.svg', string $class = '', int $width = 280): string { return ''; }
function aksrt_get_search_tab_nav_filter(string $type): string { return ''; }
function aksrt_archive_get_render_config(array $args): array { return $args + ['mode' => 'list']; }
function aksrt_render_post_list(array $args, WP_Query $query): void {
function jio_get_null($text = null, $margin = 60, string $img = 'null.svg', string $class = '', int $width = 280): string { return ''; }
function jio_get_search_tab_nav_filter(string $type): string { return ''; }
function jio_archive_get_render_config(array $args): array { return $args + ['mode' => 'list']; }
function jio_render_post_list(array $args, WP_Query $query): void {
$title = $query->posts[0]->post_title ?? '';
echo '<div class="posts-item list ajax-item">' . aksrt_renderer_highlight_text((string) $title, (string) ($args['highlight_query'] ?? '')) . '</div>';
echo '<div class="posts-item list ajax-item">' . jio_renderer_highlight_text((string) $title, (string) ($args['highlight_query'] ?? '')) . '</div>';
}
function aksrt_render_pagination($query, array $args): void {}
function aksrt_render_placeholder($kind, $count): string { return ''; }
function jio_render_pagination($query, array $args): void {}
function jio_render_placeholder($kind, $count): string { return ''; }
require ABSPATH . 'inc/helpers/posts-renderer/config.php';
require ABSPATH . 'inc/helpers/aksrt-graphic-card.php';
require ABSPATH . 'inc/helpers/aksrt-ajax-list.php';
require ABSPATH . 'inc/helpers/search/aksrt-search-box.php';
require ABSPATH . 'inc/helpers/jio-graphic-card.php';
require ABSPATH . 'inc/helpers/jio-ajax-list.php';
require ABSPATH . 'inc/helpers/search/jio-search-box.php';
function render_search_case(string $query, string $type, array $users = [], int $user_total = 0, array $posts = []): string {
$GLOBALS['search_text'] = $query;
@@ -152,13 +152,13 @@ if (($GLOBALS['last_user_query_args']['orderby'] ?? '') !== 'display_name') {
throw new RuntimeException('User name ordering is not mapped to WP_User_Query.');
}
$highlight = aksrt_renderer_highlight_text('<b>A&B</b> "Z" <script>x</script>', $query);
$highlight = jio_renderer_highlight_text('<b>A&B</b> "Z" <script>x</script>', $query);
$plain = html_entity_decode(strip_tags($highlight), ENT_QUOTES | ENT_HTML5, 'UTF-8');
if ($plain !== 'A&B "Z" x' || !str_contains($highlight, '<b class="focus-color">')) {
throw new RuntimeException('Keyword highlighting does not preserve safe text.');
}
$form_with_category = aksrt_get_search_box([
$form_with_category = jio_get_search_box([
'class' => 'main-search page-search-box',
's' => '',
'show_keywords' => false,
@@ -169,7 +169,7 @@ $form_with_category = aksrt_get_search_box([
'show_type' => true,
'in_type' => 'post',
], false);
$form_without_category = aksrt_get_search_box([
$form_without_category = jio_get_search_box([
'class' => 'main-search page-search-box',
's' => '',
'show_keywords' => false,
@@ -192,7 +192,7 @@ if (!str_contains((string) $form_with_category, 'class="line-form-input" tabinde
foreach (['post' => 'null-post.svg', 'user' => 'null-user.svg'] as $type => $image) {
$no_results = render_search_case('No matches', $type);
if (!str_contains($no_results, 'class="ajax-item aksrt-ajax-null"') || !str_contains($no_results, $image)) {
if (!str_contains($no_results, 'class="ajax-item jio-ajax-null"') || !str_contains($no_results, $image)) {
throw new RuntimeException('Empty results must remain an AJAX replaceable illustrated item.');
}
}
@@ -54,7 +54,7 @@ test('clearing history expires the saved cookie at its original WordPress path a
};
jQuery.cookie = (name, value, options) => cookies.push({ name, value, ...options });
const handler = handlerBetween('//清空搜索关键词', '//用户签到', {
jQuery, confirm: () => confirmed, aksrt__: (key) => key,
jQuery, confirm: () => confirmed, jio__: (key) => key,
});
handler.call(control);
assert.deepEqual(cookies, [{ name: 'history_search', value: null, path: '/blog/', domain: '.example.test' }]);
@@ -65,7 +65,7 @@ test('clearing history expires the saved cookie at its original WordPress path a
});
test('removed search markup has no orphaned JavaScript owner', () => {
assert.doesNotMatch(read('assets/js/core/article.js'), /initSearchClear|search-clear-btn|search-page-input|search-category-filter|aksrt_search_type|aksrt_category/);
assert.doesNotMatch(read('assets/js/core/article.js'), /initSearchClear|search-clear-btn|search-page-input|search-category-filter|jio_search_type|jio_category/);
assert.doesNotMatch(runtime, /eval\('\/' \+ val|search-tab \.item-heading/);
assert.doesNotMatch(runtime, /css\/dplayer\.min\.css/);
});
@@ -145,18 +145,18 @@ test('short or missing headings preserve pre-existing box content but clear this
assert.equal(box.firstChild, external);
assert.equal(box.querySelector('.other-widget-content').textContent, 'keep me');
assert.equal(box.querySelector('.posts-nav-lists'), null);
assert.equal(box.dataset.aksrtSectionNav, undefined);
assert.equal(box.dataset.jioSectionNav, undefined);
const third = addHeading(runtime.document, rootNode, 'h2', 'Three');
runtime.autoFun();
assert.equal(box.dataset.aksrtSectionNav, '1');
assert.equal(box.dataset.jioSectionNav, '1');
assert.ok(box.querySelector('.posts-nav-lists'));
third.remove();
runtime.autoFun();
assert.equal(box.querySelector('.posts-nav-lists'), null);
assert.equal(box.children.length, 0);
assert.equal(box.dataset.aksrtSectionNav, undefined);
assert.equal(box.dataset.jioSectionNav, undefined);
assert.ok(first.parentNode && second.parentNode);
});
+8 -8
View File
@@ -84,12 +84,12 @@ function setup({ buttons = [makeButton('btn uc-header__signin-btn', '签到')],
};
const jq = { off() { return this; }, on(event, callback) { listeners[event] = callback; return this; } };
const window = {
aksrtData: { captchaSignin, signinDetailsUrl: '/wp-json/aksrt/v1/signin/details', currencyName },
AksrtModalRuntime: { open: (...args) => modals.push(args), openRemote: (...args) => modals.push(args) },
AksrtRequest: { request(endpoint, options) { calls.push({ endpoint, options }); return endpoint === '/signin/today' ? today : signin; } },
jioData: { captchaSignin, signinDetailsUrl: '/wp-json/jio/v1/signin/details', currencyName },
JioModalRuntime: { open: (...args) => modals.push(args), openRemote: (...args) => modals.push(args) },
JioRequest: { request(endpoint, options) { calls.push({ endpoint, options }); return endpoint === '/signin/today' ? today : signin; } },
};
const captchaCalls = [];
if (captchaRun) window.AKSRTCaptcha = {
if (captchaRun) window.JioCaptcha = {
run(scene, scope) { captchaCalls.push(['run', scene, scope]); return captchaRun; },
reset(scene, scope) { captchaCalls.push(['reset', scene, scope]); },
};
@@ -147,8 +147,8 @@ test('uses reference loading, success toast, and replaces all entry points with
await settle();
assert.deepEqual(h.notices.find((notice) => notice[1] === 'success')?.slice(0, 2), ['签到成功,积分 +2', 'success']);
for (const original of [first, second]) {
assert.equal(original.replaced?.getAttribute('data-aksrt-modal'), 'remote');
assert.equal(original.replaced?.getAttribute('data-aksrt-modal-url'), '/wp-json/aksrt/v1/signin/details');
assert.equal(original.replaced?.getAttribute('data-jio-modal'), 'remote');
assert.equal(original.replaced?.getAttribute('data-jio-modal-url'), '/wp-json/jio/v1/signin/details');
assert.match(original.replaced.innerHTML, /已签到/);
assert.doesNotMatch(original.replaced.innerHTML, /data-signin-label/);
assert.match(original.replaced.className, /checkin-details-link/);
@@ -271,7 +271,7 @@ test('leaves a disabled sign-in feature disabled without turning it into a signe
test('does not send the request when captcha is absent or cancelled and keeps original markup', async () => {
const missingButton = makeButton('btn', '签到');
const missing = setup({ buttons: [missingButton], captchaSignin: true });
missing.window.AKSRTCaptcha = undefined;
missing.window.JioCaptcha = undefined;
// No mount is needed in the harness to verify the missing provider path.
missingButton.click();
await settle();
@@ -292,7 +292,7 @@ test('does not bind the same button twice through repeated auto_fun scans', asyn
const button = makeButton('btn', '签到');
const h = setup({ buttons: [button] });
await settle();
h.listeners['auto_fun.aksrt-signin']();
h.listeners['auto_fun.jio-signin']();
assert.equal(button.listeners.click.length, 1);
button.click();
await settle();
@@ -13,8 +13,8 @@ const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const singleRender = read('inc/helpers/aksrt-single-render.php');
const sliderHelper = read('inc/helpers/aksrt-slider.php');
const singleRender = read('inc/helpers/jio-single-render.php');
const sliderHelper = read('inc/helpers/jio-slider.php');
test('single cover: image mode keeps the calc(scale% + 50px) height', () => {
assert.match(
@@ -51,7 +51,7 @@ test('single cover: slide mode reserves the same +50px via the slider helper', (
assert.match(
singleRender,
/\$slider_args\['scale_extra'\] = '50px';/,
'比例高度分支必须向 aksrt_render_slider 传 scale_extra=50px'
'比例高度分支必须向 jio_render_slider 传 scale_extra=50px'
);
assert.match(
singleRender,
@@ -74,8 +74,8 @@ test('single cover: slide mode reserves the same +50px via the slider helper', (
test('single cover: the dead static slide fallback is gone', () => {
assert.doesNotMatch(
singleRender,
/function_exists\('aksrt_render_slider'\)/,
'aksrt_render_slider 由 bootstrap 无条件加载,不得保留 function_exists 双路判断'
/function_exists\('jio_render_slider'\)/,
'jio_render_slider 由 bootstrap 无条件加载,不得保留 function_exists 双路判断'
);
assert.doesNotMatch(
singleRender,
@@ -107,7 +107,7 @@ test('video cover: the player video fills and center-crops like the image cover'
const player = read('assets/js/video-player.js');
assert.match(
player,
/\.aksrt-video video\{display:block;width:100%;height:100%;object-fit:cover;\}/,
/\.jio-video video\{display:block;width:100%;height:100%;object-fit:cover;\}/,
'视频必须铺满封面比例盒并居中裁切'
);
assert.doesNotMatch(
@@ -180,7 +180,7 @@ test('resource singles get the cover leaf versioned, never via the aggregate @im
);
assert.match(
enqueue,
/'aksrt-single-post-cover'/,
/'jio-single-post-cover'/,
'cover leaf 句柄与文章分支命名保持一致'
);
});
@@ -48,7 +48,7 @@ test('security settings are consumed at runtime and initialized on boot', () =>
const bootstrap = read('functions.php');
assert.match(runtime, /public static function get_security_settings\(\): array/);
assert.match(bootstrap, /\\AKSRT\\Extensions\\SecurityHardening::get_instance\(\)->init\(\)/);
assert.match(bootstrap, /\\Jio\\Extensions\\SecurityHardening::get_instance\(\)->init\(\)/);
});
test('hardening switches wire the documented WordPress hooks', () => {
@@ -74,7 +74,7 @@ test('verification-code sends are funneled through the configurable send limiter
assert.match(sms, /SendLimit::check\(\$phone\)/);
assert.match(sms, /SendLimit::record\(\$phone\)/);
assert.doesNotMatch(sms, /RATE_LIMIT|DAILY_LIMIT|aksrt_sms_rate_|aksrt_sms_daily_/);
assert.doesNotMatch(sms, /RATE_LIMIT|DAILY_LIMIT|jio_sms_rate_|jio_sms_daily_/);
assert.match(sendLimit, /security_captcha_send_cooldown/);
assert.match(sendLimit, /security_captcha_to_hour/);
assert.match(sendLimit, /security_captcha_to_day/);
@@ -104,17 +104,17 @@ test('content brush limits are wired into post, comment, upload, register, and o
});
test('phone masking is shared by the account UI and adapts to international numbers', () => {
const helper = read('inc/helpers/aksrt-phone.php');
const helper = read('inc/helpers/jio-phone.php');
const bootstrap = read('inc/bootstrap.php');
const template = read('inc/helpers/user-center-renderers/settings-account.php');
assert.match(bootstrap, /inc\/helpers\/aksrt-phone\.php/);
assert.match(helper, /function aksrt_phone_mask_parts\(string \$phone\): \?array/);
assert.match(helper, /function aksrt_phone_mask_display\(string \$phone\): string/);
assert.match(bootstrap, /inc\/helpers\/jio-phone\.php/);
assert.match(helper, /function jio_phone_mask_parts\(string \$phone\): \?array/);
assert.match(helper, /function jio_phone_mask_display\(string \$phone\): string/);
assert.match(helper, /min\(11, max\(4, \$length - 7\)\)/);
assert.doesNotMatch(template, /substr_replace\(\$account_phone, '\*\*\*\*', 3, 4\)/);
assert.match(template, /aksrt_phone_mask_parts\(\$account_phone\)/);
assert.match(template, /aksrt_phone_mask_display\(\$account_phone\)/);
assert.match(template, /jio_phone_mask_parts\(\$account_phone\)/);
assert.match(template, /jio_phone_mask_display\(\$account_phone\)/);
});
test('site security runtime contract harness passes', () => {
+42 -42
View File
@@ -8,33 +8,33 @@ namespace {
define('MINUTE_IN_SECONDS', 60);
define('HOUR_IN_SECONDS', 3600);
define('DAY_IN_SECONDS', 86400);
$GLOBALS['aksrt_security_settings'] = [];
$GLOBALS['aksrt_security_transients'] = [];
$GLOBALS['aksrt_security_user_id'] = 7;
$GLOBALS['jio_security_settings'] = [];
$GLOBALS['jio_security_transients'] = [];
$GLOBALS['jio_security_user_id'] = 7;
$_SERVER['REMOTE_ADDR'] = '203.0.113.9';
function __(string $text, string $domain = ''): string { return $text; }
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
function get_current_user_id(): int { return (int) $GLOBALS['aksrt_security_user_id']; }
function get_current_user_id(): int { return (int) $GLOBALS['jio_security_user_id']; }
function current_user_can(string $capability): bool { return false; }
function wp_date(string $format): string { return date($format); }
function add_filter(string $hook, callable $callback, int $priority = 10, int $args = 1): void {}
function add_action(string $hook, callable $callback, int $priority = 10, int $args = 1): void {}
function set_transient(string $key, mixed $value, int $expiration = 0): bool {
$GLOBALS['aksrt_security_transients'][$key] = ['value' => $value, 'expires' => $expiration > 0 ? time() + $expiration : 0];
$GLOBALS['jio_security_transients'][$key] = ['value' => $value, 'expires' => $expiration > 0 ? time() + $expiration : 0];
return true;
}
function get_transient(string $key): mixed {
$row = $GLOBALS['aksrt_security_transients'][$key] ?? null;
$row = $GLOBALS['jio_security_transients'][$key] ?? null;
if (!is_array($row)) return false;
if (!empty($row['expires']) && $row['expires'] <= time()) {
unset($GLOBALS['aksrt_security_transients'][$key]);
unset($GLOBALS['jio_security_transients'][$key]);
return false;
}
return $row['value'] ?? false;
}
function delete_transient(string $key): bool {
unset($GLOBALS['aksrt_security_transients'][$key]);
unset($GLOBALS['jio_security_transients'][$key]);
return true;
}
@@ -43,17 +43,17 @@ namespace {
}
function site_security_brush_minute_key(string $scope, string $subject): string {
return sprintf('aksrt_brush_%s_m_%s_%s%d', $scope, md5($subject), wp_date('YmdH'), intdiv((int) wp_date('i'), 10));
return sprintf('jio_brush_%s_m_%s_%s%d', $scope, md5($subject), wp_date('YmdH'), intdiv((int) wp_date('i'), 10));
}
function site_security_brush_day_key(string $scope, string $subject): string {
return sprintf('aksrt_brush_%s_d_%s_%s', $scope, md5($subject), wp_date('Ymd'));
return sprintf('jio_brush_%s_d_%s_%s', $scope, md5($subject), wp_date('Ymd'));
}
require dirname(__DIR__, 2) . '/src/Settings/ExtensionRuntime.php';
require dirname(__DIR__, 2) . '/src/Extensions/SecurityHardening.php';
require dirname(__DIR__, 2) . '/src/Extensions/SendLimit.php';
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-phone.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-phone.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsIP.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsAudit.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsPush.php';
@@ -62,37 +62,37 @@ namespace {
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensions.php';
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function get_all(): array { return $GLOBALS['aksrt_security_settings']; }
public static function get_all(): array { return $GLOBALS['jio_security_settings']; }
}
}
namespace {
use AKSRT\Admin\Schema\SchemaExtensions;
use AKSRT\Extensions\SecurityHardening;
use AKSRT\Extensions\SendLimit;
use AKSRT\Settings\ExtensionRuntime;
use Jio\Admin\Schema\SchemaExtensions;
use Jio\Extensions\SecurityHardening;
use Jio\Extensions\SendLimit;
use Jio\Settings\ExtensionRuntime;
// 1) Phone masking: domestic hides 4 digits, international hides length-7 (min 4), short numbers are unmaskable.
$domestic = aksrt_phone_mask_parts('13812341234');
$domestic = jio_phone_mask_parts('13812341234');
site_security_check($domestic !== null, 'domestic number should be maskable');
site_security_check($domestic['prefix'] === '138' && $domestic['hidden'] === '1234' && $domestic['suffix'] === '1234', 'domestic mask split is wrong');
site_security_check($domestic['hidden_length'] === 4, 'domestic mask should hide 4 digits');
site_security_check(aksrt_phone_mask_display('13812341234') === '138****1234', 'domestic masked display is wrong');
site_security_check(jio_phone_mask_display('13812341234') === '138****1234', 'domestic masked display is wrong');
$intl = aksrt_phone_mask_parts('+8613812341234');
$intl = jio_phone_mask_parts('+8613812341234');
site_security_check($intl !== null, 'international number should be maskable');
site_security_check($intl['hidden_length'] === 6, 'international mask should hide length-7 digits');
site_security_check($intl['hidden'] === '381234', 'international hidden segment is wrong');
site_security_check($intl['leading_plus'] === true, 'leading plus flag is wrong');
site_security_check(aksrt_phone_mask_display('+8613812341234') === '+861******1234', 'international masked display is wrong');
site_security_check(jio_phone_mask_display('+8613812341234') === '+861******1234', 'international masked display is wrong');
$eight = aksrt_phone_mask_parts('12345678');
$eight = jio_phone_mask_parts('12345678');
site_security_check($eight !== null && $eight['hidden_length'] === 4 && $eight['suffix'] === '8', 'short mask should still hide 4 digits with a 1-digit suffix');
site_security_check(aksrt_phone_mask_parts('1234567') === null, 'numbers with fewer than 8 digits must be unmaskable');
site_security_check(aksrt_phone_mask_parts('') === null, 'empty number must be unmaskable');
site_security_check(aksrt_phone_mask_display('1234567') === '1234567', 'unmaskable numbers must display raw');
site_security_check(jio_phone_mask_parts('1234567') === null, 'numbers with fewer than 8 digits must be unmaskable');
site_security_check(jio_phone_mask_parts('') === null, 'empty number must be unmaskable');
site_security_check(jio_phone_mask_display('1234567') === '1234567', 'unmaskable numbers must display raw');
// 2) Brush limits: 10-minute and daily caps, 0 = unlimited, admin exempt handled by callers.
$allowed = SecurityHardening::brush_allow('post', '12', 2, 0);
@@ -101,12 +101,12 @@ namespace {
site_security_check((int) get_transient(site_security_brush_minute_key('post', '12')) === 2, 'post attempts were not counted');
site_security_check(SecurityHardening::brush_allow('post', '12', 2, 0) !== true, 'post 10-minute cap was not enforced');
$GLOBALS['aksrt_security_transients'] = [];
$GLOBALS['jio_security_transients'] = [];
site_security_check(SecurityHardening::brush_allow('post', '12', 0, 2) === true, 'first daily post was blocked');
site_security_check(SecurityHardening::brush_allow('post', '12', 0, 2) === true, 'second daily post was blocked');
site_security_check(SecurityHardening::brush_allow('post', '12', 0, 2) !== true, 'post daily cap was not enforced');
$GLOBALS['aksrt_security_transients'] = [];
$GLOBALS['jio_security_transients'] = [];
site_security_check(SecurityHardening::brush_allow('order', '203.0.113.9', 0, 0) === true, 'disabled caps should never block');
site_security_check(get_transient(site_security_brush_minute_key('order', '203.0.113.9')) === false, 'disabled caps should not count attempts');
@@ -128,13 +128,13 @@ namespace {
site_security_check(SecurityHardening::error_frequency_wait() === 0, 'error frequency clear did not reset the counter');
// Disabling the switch must silence the limit.
$GLOBALS['aksrt_security_settings']['security_error_frequency_limit'] = false;
$GLOBALS['jio_security_settings']['security_error_frequency_limit'] = false;
SecurityHardening::error_frequency_record();
site_security_check(SecurityHardening::error_frequency_wait() === 0, 'disabling security_error_frequency_limit did not silence the limiter');
$GLOBALS['aksrt_security_settings'] = [];
$GLOBALS['jio_security_settings'] = [];
// 4) Send limit: cooldown, recipient hourly/daily, IP 10-minute/daily caps; 0 = unlimited.
$GLOBALS['aksrt_security_transients'] = [];
$GLOBALS['jio_security_transients'] = [];
$first = SendLimit::check('user@example.test');
site_security_check($first['ok'] === true, 'first verification send was blocked');
site_security_check(SendLimit::record('user@example.test') === 60, 'send limit did not return the cooldown');
@@ -142,30 +142,30 @@ namespace {
site_security_check($second['ok'] === false && $second['remaining'] > 0, 'send cooldown was not enforced');
site_security_check(SendLimit::check('other@example.test')['ok'] === true, 'send cooldown leaked across recipients');
$GLOBALS['aksrt_security_transients'] = [];
$GLOBALS['jio_security_transients'] = [];
for ($send = 0; $send < 15; $send++) {
site_security_check(SendLimit::record('user@example.test') === 60, 'send record failed');
}
site_security_check(SendLimit::check('user@example.test')['ok'] === false, 'recipient hourly cap was not enforced');
site_security_check(SendLimit::check('user2@example.test')['ok'] === true, 'recipient hourly cap leaked across recipients');
$GLOBALS['aksrt_security_transients'] = [];
$GLOBALS['jio_security_transients'] = [];
for ($send = 0; $send < 20; $send++) {
SendLimit::record('user@example.test');
}
site_security_check(SendLimit::check('user2@example.test')['ok'] === false, 'IP 10-minute cap was not enforced');
$GLOBALS['aksrt_security_transients'] = [];
$GLOBALS['aksrt_security_settings']['security_captcha_send_cooldown'] = 0;
$GLOBALS['aksrt_security_settings']['security_captcha_to_hour'] = 0;
$GLOBALS['aksrt_security_settings']['security_captcha_to_day'] = 0;
$GLOBALS['aksrt_security_settings']['security_captcha_ip_10min'] = 0;
$GLOBALS['aksrt_security_settings']['security_captcha_ip_day'] = 0;
$GLOBALS['jio_security_transients'] = [];
$GLOBALS['jio_security_settings']['security_captcha_send_cooldown'] = 0;
$GLOBALS['jio_security_settings']['security_captcha_to_hour'] = 0;
$GLOBALS['jio_security_settings']['security_captcha_to_day'] = 0;
$GLOBALS['jio_security_settings']['security_captcha_ip_10min'] = 0;
$GLOBALS['jio_security_settings']['security_captcha_ip_day'] = 0;
for ($send = 0; $send < 100; $send++) {
site_security_check(SendLimit::record('user@example.test') === 0, 'disabled send caps should not cooldown');
}
site_security_check(SendLimit::check('user@example.test')['ok'] === true, 'disabled send caps still blocked sends');
$GLOBALS['aksrt_security_settings'] = [];
$GLOBALS['jio_security_settings'] = [];
// 5) Runtime defaults mirror the schema defaults.
$security = ExtensionRuntime::get_security_settings();
@@ -181,12 +181,12 @@ namespace {
site_security_check($security['security_disabled_pingback'] === true && $security['security_disabled_xmlrpc'] === true, 'hardening switch defaults are wrong');
site_security_check($security['security_user_disable_admin'] === true && $security['close_attachment_page'] === true, 'access switch defaults are wrong');
$GLOBALS['aksrt_security_settings']['security_brush_post_10min'] = '35';
$GLOBALS['aksrt_security_settings']['close_attachment_page'] = 0;
$GLOBALS['jio_security_settings']['security_brush_post_10min'] = '35';
$GLOBALS['jio_security_settings']['close_attachment_page'] = 0;
$stored = ExtensionRuntime::get_security_settings();
site_security_check($stored['security_brush_post_10min'] === 35, 'stored cap override was not honored');
site_security_check($stored['close_attachment_page'] === false, 'stored switch override was not honored');
$GLOBALS['aksrt_security_settings'] = [];
$GLOBALS['jio_security_settings'] = [];
// 6) Schema: security section is registered, defaults match, and no 人机验证 fields exist.
$extensions = SchemaExtensions::build();
+11 -11
View File
@@ -6,7 +6,7 @@ declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
define('HOUR_IN_SECONDS', 3600);
define('WP_CONTENT_DIR', sys_get_temp_dir() . '/aksrt-split-test-' . getmypid());
define('WP_CONTENT_DIR', sys_get_temp_dir() . '/jio-split-test-' . getmypid());
function sanitize_file_name(string $name): string { return preg_replace('/[^A-Za-z0-9._\-]/', '', $name) ?? ''; }
function __(string $text, string $domain = ''): string { return $text; }
@@ -63,9 +63,9 @@ namespace {
require dirname(__DIR__, 2) . '/src/Core/FrontSubmitChunk.php';
}
namespace AKSRT\Core {
namespace Jio\Core {
final class FakeFrontSubmit {
use \AKSRT\Core\FrontSubmitChunk;
use \Jio\Core\FrontSubmitChunk;
public static int $limit_bytes = 10 * 1048576;
public static array $persisted = [];
@@ -83,10 +83,10 @@ namespace AKSRT\Core {
}
namespace {
use AKSRT\Core\FakeFrontSubmit;
use Jio\Core\FakeFrontSubmit;
$submit = new FakeFrontSubmit();
$temp_dir = WP_CONTENT_DIR . '/aksrt-temp';
$temp_dir = WP_CONTENT_DIR . '/jio-temp';
// 1) Chunk parameter validation.
$err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => '', 'chunks' => 3, 'chunk' => 0, 'size' => 10]));
@@ -124,12 +124,12 @@ namespace {
// 4) Merge refuses when a chunk is missing.
$err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_missing', 'merge should refuse with missing chunks');
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'jio_chunk_missing', 'merge should refuse with missing chunks');
// 5) Merge with a size mismatch is refused.
$submit->rest_upload_chunk(new FakeRestRequest($params(1, 3, 6), ['file' => ['tmp_name' => $make_file('BBB'), 'error' => 0]]));
$err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 999]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_size_mismatch', 'size mismatch should refuse the merge');
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'jio_chunk_size_mismatch', 'size mismatch should refuse the merge');
// 6) Happy-path merge concatenates in order, flags sideload, and cleans up.
$response = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9]));
@@ -147,20 +147,20 @@ namespace {
// 8) Chunk size cap refuses oversized chunk blobs.
$big = $make_file(str_repeat('x', 3 * 1048576));
$err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 1, 3 * 1048576), ['file' => ['tmp_name' => $big, 'error' => 0]]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_invalid_size', 'oversized chunk blobs should be refused');
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'jio_chunk_invalid_size', 'oversized chunk blobs should be refused');
// 9) Kind size limit caps cumulative uploads.
FakeFrontSubmit::$limit_bytes = 4;
$err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 2, 9), ['file' => ['tmp_name' => $make_file('AAAAA'), 'error' => 0]]));
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_upload_too_large', 'cumulative size over the kind limit should be refused');
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'jio_upload_too_large', 'cumulative size over the kind limit should be refused');
// Cleanup.
FakeFrontSubmit::$persisted = [];
foreach ((array) glob(WP_CONTENT_DIR . '/*.part') as $leftover) {
@unlink((string) $leftover);
}
@unlink(WP_CONTENT_DIR . '/aksrt-temp/index.php');
@rmdir(WP_CONTENT_DIR . '/aksrt-temp');
@unlink(WP_CONTENT_DIR . '/jio-temp/index.php');
@rmdir(WP_CONTENT_DIR . '/jio-temp');
@rmdir(WP_CONTENT_DIR);
echo "Split upload harness passed\n";
@@ -56,8 +56,8 @@ test('system tools settings are consumed at runtime and initialized on boot', ()
assert.match(runtime, /public static function get_system_tools_settings\(\): array/);
assert.match(runtime, /'newfilename_type'\s+=> \$rename_type/);
assert.match(runtime, /'split_minimum_size'\s+=> max\(4, min\(100, \(int\) \(\$settings\['split_minimum_size'\] \?\? 20\)\)\)/);
assert.match(bootstrap, /\\AKSRT\\Extensions\\SystemTools::get_instance\(\)->init\(\)/);
assert.match(bootstrap, /\\AKSRT\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/);
assert.match(bootstrap, /\\Jio\\Extensions\\SystemTools::get_instance\(\)->init\(\)/);
assert.match(bootstrap, /\\Jio\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/);
});
test('system tools switches wire the reference theme hook suite', () => {
@@ -121,14 +121,14 @@ test('split upload ships a full chunk protocol: routes, merge pipeline, and clie
assert.match(chunk, /const CHUNK_SIZE_BYTES = 2 \* 1048576;/);
assert.match(chunk, /md5\(get_current_blog_id\(\) \. '\|' \. get_current_user_id\(\) \. '\|' \. \$name\)/);
assert.match(chunk, /\$this->persist_uploaded_file\(\$params\['kind'\], \[/);
assert.match(chunk, /apply_filters\('aksrt_temp_dir', WP_CONTENT_DIR \. '\/aksrt-temp'\)/);
assert.match(chunk, /apply_filters\('jio_temp_dir', WP_CONTENT_DIR \. '\/jio-temp'\)/);
assert.match(chunk, /\$this->chunk_temp_cleanup\(\$temp_dir\)/);
// Whole-file upload and chunk merge share one persistence pipeline; merged
// files are not HTTP uploads, so the pipeline must use the sideload action.
assert.match(frontSubmit, /return \$this->persist_uploaded_file\(\$kind, \$file, false\);/);
assert.match(frontSubmit, /'action' => 'wp_handle_sideload'/);
assert.match(frontSubmit, /media_handle_upload\('aksrt_front_submit_file', 0, \[\], \$overrides\)/);
assert.match(frontSubmit, /media_handle_upload\('jio_front_submit_file', 0, \[\], \$overrides\)/);
// Client uploader picks the chunked path past the threshold and resumes from status.
assert.match(editorJs, /function uploadInChunks\(file, type\)/);
@@ -139,7 +139,7 @@ test('split upload ships a full chunk protocol: routes, merge pipeline, and clie
assert.match(editorJs, /chunkUrl\('upload-merge'\)/);
assert.match(editorJs, /'\/front-submit\/' \+ action \+ '\/' \+ uploadKey/);
// Config flows from the settings page into aksrtSubmitData.
// Config flows from the settings page into jioSubmitData.
assert.match(submitPage, /'splitUpload' => \[/);
assert.match(submitPage, /'enabled' => !empty\(\$system_tools\['split_upload_s'\]\)/);
assert.match(submitPage, /'minSize' => \(int\) \(\$system_tools\['split_minimum_size'\] \?\? 20\)/);
@@ -152,11 +152,11 @@ test('split upload ships a full chunk protocol: routes, merge pipeline, and clie
test('frontend page edit ships gate, footer modal, and legacy ajax-save contract', () => {
const pageAdminSet = read('src/Frontend/PageAdminSet.php');
const floatButtons = read('inc/helpers/aksrt-float-buttons.php');
const floatButtons = read('inc/helpers/jio-float-buttons.php');
const bootstrap = read('functions.php');
const mainJs = read('assets/js/runtime/main.js');
assert.match(bootstrap, /\\AKSRT\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/);
assert.match(bootstrap, /\\Jio\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/);
assert.match(pageAdminSet, /public static function enabled\(\): bool/);
assert.match(pageAdminSet, /empty\(\$settings\['admin_frontend_set'\]/);
assert.match(pageAdminSet, /current_user_can\('manage_options'\)/);
@@ -164,15 +164,15 @@ test('frontend page edit ships gate, footer modal, and legacy ajax-save contract
assert.match(pageAdminSet, /is_page_template\('templates\/pages\/page-posts-navs\.php'\)/);
// Reference parity: the gear float button opens the footer-rendered bootstrap modal.
assert.match(floatButtons, /apply_filters\('aksrt_float_right_buttons', \$btn, \$is_mobile\)/);
assert.match(pageAdminSet, /add_filter\('aksrt_float_right_buttons', \[\$this, 'float_button'\], 10, 2\)/);
assert.match(floatButtons, /apply_filters\('jio_float_right_buttons', \$btn, \$is_mobile\)/);
assert.match(pageAdminSet, /add_filter\('jio_float_right_buttons', \[\$this, 'float_button'\], 10, 2\)/);
assert.match(pageAdminSet, /add_action\('wp_footer', \[\$this, 'render_modal'\], 10\)/);
assert.match(pageAdminSet, /data-toggle="modal" data-target="#modal_admin_set"/);
assert.match(pageAdminSet, /class="modal fade" id="modal_admin_set" tabindex="-1" role="dialog" aria-hidden="false" win-ajax-replace="modal_admin_set"/);
assert.match(pageAdminSet, /class="modal-content page-set-modal"/);
assert.match(pageAdminSet, /wp_nonce_field\('aksrt_frontend_set', '_wpnonce', true, false\)/);
assert.match(pageAdminSet, /wp_nonce_field\('jio_frontend_set', '_wpnonce', true, false\)/);
// Reference hidden inputs: type + taxonomy + post_type + id travel with the submit.
assert.match(pageAdminSet, /name="action" value="aksrt_frontend_set_save"/);
assert.match(pageAdminSet, /name="action" value="jio_frontend_set_save"/);
assert.match(pageAdminSet, /name="type" value="' . esc_attr\(\$type\)/);
assert.match(pageAdminSet, /name="taxonomy" value="' . esc_attr\(\$taxonomy\)/);
assert.match(pageAdminSet, /name="post_type" value="' . esc_attr\(\$post_type\)/);
@@ -181,25 +181,25 @@ test('frontend page edit ships gate, footer modal, and legacy ajax-save contract
assert.match(pageAdminSet, /'autofocus' => \['panel' => 'widgets'\]/);
// Modal footer mirrors the reference: 模块布局 / 后台编辑 / 确认修改 (wp-ajax-submit).
assert.match(pageAdminSet, /esc_html__\('模块布局', 'aksrt'\)/);
assert.match(pageAdminSet, /esc_html__\('后台编辑', 'aksrt'\)/);
assert.match(pageAdminSet, /esc_html__\('确认修改', 'aksrt'\)/);
assert.match(pageAdminSet, /esc_html__\('模块布局', 'jio'\)/);
assert.match(pageAdminSet, /esc_html__\('后台编辑', 'jio'\)/);
assert.match(pageAdminSet, /esc_html__\('确认修改', 'jio'\)/);
assert.match(pageAdminSet, /class="but c-yellow-2 padding-lg"/);
assert.match(pageAdminSet, /class="but c-yellow padding-lg"/);
assert.match(pageAdminSet, /class="but jb-blue padding-lg wp-ajax-submit"/);
// Fields replicate the reference input construct and map to AKSRT's post meta.
// Fields replicate the reference input construct and map to Jio's post meta.
assert.match(pageAdminSet, /private function render_inputs\(array \$inputs\): string/);
assert.match(pageAdminSet, /class="heading col-xs-3 text-right"/);
assert.match(pageAdminSet, /class="option col-xs-8"/);
assert.match(pageAdminSet, /\\AKSRT\\Core\\PostMetaBox::META_KEY/);
assert.match(pageAdminSet, /\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/);
assert.match(pageAdminSet, /update_post_meta\(\$post_id, '_aksrt_like_count', max\(0, \(int\) \$_POST\['like'\]\)\)/);
assert.match(pageAdminSet, /\\Jio\\Core\\PostMetaBox::META_KEY/);
assert.match(pageAdminSet, /\\Jio\\Core\\DomainModel::POST_VIEW_COUNT_KEY/);
assert.match(pageAdminSet, /update_post_meta\(\$post_id, '_jio_like_count', max\(0, \(int\) \$_POST\['like'\]\)\)/);
assert.match(pageAdminSet, /wp_update_term\(\$term_id, \(string\) \$term->taxonomy, \$data\)/);
assert.match(pageAdminSet, /set_post_format\(\$post_id, \$format === 'standard' \? '' : \$format\)/);
// 文章格式 mirrors the reference's fixed four-option list, not the theme's
// wider add_theme_support('post-formats') registration.
assert.match(pageAdminSet, /return \[\s*'standard' => __\('标准', 'aksrt'\),\s*'image' => __\('图像', 'aksrt'\),\s*'gallery' => __\('画廊', 'aksrt'\),\s*'video' => __\('视频', 'aksrt'\),\s*\];/s);
assert.match(pageAdminSet, /return \[\s*'standard' => __\('标准', 'jio'\),\s*'image' => __\('图像', 'jio'\),\s*'gallery' => __\('画廊', 'jio'\),\s*'video' => __\('视频', 'jio'\),\s*\];/s);
assert.doesNotMatch(pageAdminSet, /get_theme_support\('post-formats'\)\s*\?/);
assert.doesNotMatch(pageAdminSet, /get_post_format_string\(/);
// Page branch: 标题样式 + 模块布局(widgets_register) + 容器位置.
@@ -213,10 +213,10 @@ test('frontend page edit ships gate, footer modal, and legacy ajax-save contract
// The geometry mirrors the reference construct (row + heading + option, \n separators).
assert.match(pageAdminSet, /'<div class="mb10 row' \. esc_attr\(\$class\) \. '">' \. "\\n"/);
// Save runs under the vendored main.js aksrt_ajax contract ({msg,reload}).
assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_save', \[\$this, 'ajax_save'\]\)/);
assert.match(pageAdminSet, /check_ajax_referer\('aksrt_frontend_set', '_wpnonce'\)/);
assert.match(pageAdminSet, /wp_send_json\(\['msg' => __\('保存成功,正在刷新页面', 'aksrt'\), 'reload' => true\]\)/);
// Save runs under the vendored main.js jio_ajax contract ({msg,reload}).
assert.match(pageAdminSet, /add_action\('wp_ajax_jio_frontend_set_save', \[\$this, 'ajax_save'\]\)/);
assert.match(pageAdminSet, /check_ajax_referer\('jio_frontend_set', '_wpnonce'\)/);
assert.match(pageAdminSet, /wp_send_json\(\['msg' => __\('保存成功,正在刷新页面', 'jio'\), 'reload' => true\]\)/);
assert.match(mainJs, /jQuery\.fn\.serializeObject = function/);
assert.match(mainJs, /if \(n\.reload\) \{/);
// The dedicated fetch-submit client is gone: vendored main.js owns the flow.
@@ -230,7 +230,7 @@ test('frontend page edit icons follow the FA5 convention and the hint bar fades
const publicCss = read('assets/css/build/public.css');
const publicMinCss = read('assets/css/build/public.min.css');
// Site-wide icon convention is aksrt_icon_markup()'s normalized `fa fas fa-*`:
// Site-wide icon convention is jio_icon_markup()'s normalized `fa fas fa-*`:
// the gear, the modal header, both footer links and the submit button.
// FA5 renames the reference's FA4 glyphs (sliders→sliders-h, pie-chart→chart-pie).
assert.match(pageAdminSet, /<i class="fa fas fa-cog fa-spin" aria-hidden="true"><\/i>/);
@@ -256,14 +256,14 @@ test('frontend page edit icons follow the FA5 convention and the hint bar fades
// The transient label fades 2s after load over 1s (reference js/page-edit.js).
assert.match(pageAdminSet, /add_action\('wp_footer', \[self::class, 'enqueue_hint_script'\], 9\)/);
assert.match(pageAdminSet, /wp_enqueue_script\(\s*'aksrt-front-page-admin-set',\s*get_template_directory_uri\(\) \. '\/assets\/js\/front-page-admin-set\.js',\s*\['jquery'\],\s*\(string\) filemtime\(\$path\),\s*true\s*\)/s);
assert.match(pageAdminSet, /wp_enqueue_script\(\s*'jio-front-page-admin-set',\s*get_template_directory_uri\(\) \. '\/assets\/js\/front-page-admin-set\.js',\s*\['jquery'\],\s*\(string\) filemtime\(\$path\),\s*true\s*\)/s);
assert.match(hintJs, /setTimeout\(function \(\) \{/, 'the fade is delayed like the reference');
assert.match(hintJs, /hint\.style\.transition = 'opacity 1s'/);
assert.match(hintJs, /hint\.style\.opacity = '0'/);
assert.match(hintJs, /querySelector\('\.admin-set-page \.admin-set-info'\)/);
// Label geometry: reference .abs-right is top:50% + right:.7em + translateY(-50%).
// AKSRT's legacy .abs-right is top:0/right:0, so the float bar restores it.
// Jio's legacy .abs-right is top:0/right:0, so the float bar restores it.
// The minified bundle has no space before the brace, so keep it optional.
const labelRule = /\.float-right \.admin-set-page \.admin-set-info\s*\{[^}]*\}/s;
assert.match(floatingCss, labelRule);
@@ -306,16 +306,16 @@ test('float toolbar config reaches the tinymce plugin with reference-context but
assert.match(editorJs, /if \(editorCfg\.floatToolbar\) \{\s*\n\s*registerFloatToolbar\(editor\);/);
assert.match(editorJs, /editor\.on\('wptoolbar', function \(event\)/);
assert.match(editorJs, /editor\.wp\._createToolbar\(bars\)/);
assert.match(editorJs, /editor\.addButton\('aksrt_h2'/);
assert.match(editorJs, /editor\.addButton\('aksrt_h3'/);
assert.match(editorJs, /editor\.addButton\('aksrt_remove'/);
assert.match(editorJs, /editor\.addButton\('jio_h2'/);
assert.match(editorJs, /editor\.addButton\('jio_h3'/);
assert.match(editorJs, /editor\.addButton\('jio_remove'/);
assert.match(editorJs, /function floatToolbarButtons\(editor, event\)/);
assert.match(schema, /'mce_float_toolbar', 'label' => '经典编辑器增强浮动工具栏', 'type' => 'checkbox'/);
// The editor template still wires the plugin into wp_editor.
assert.match(editorTemplate, /aksrt_editor_tools/);
assert.match(editorTemplate, /jio_editor_tools/);
});
test('countdown display switch gates aksrt_time_ago with the custom format fallback', () => {
test('countdown display switch gates jio_time_ago with the custom format fallback', () => {
const helper = read('inc/helpers/template-tags/search-meta.php');
const coreHooks = read('inc/core-hooks.php');
+41 -41
View File
@@ -6,31 +6,31 @@ declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
$GLOBALS['aksrt_system_settings'] = [];
$GLOBALS['aksrt_system_added'] = [];
$GLOBALS['aksrt_system_removed'] = [];
$GLOBALS['jio_system_settings'] = [];
$GLOBALS['jio_system_added'] = [];
$GLOBALS['jio_system_removed'] = [];
function __(string $text, string $domain = ''): string { return $text; }
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
function is_super_admin(): bool { return (bool) $GLOBALS['aksrt_system_is_super_admin']; }
function is_super_admin(): bool { return (bool) $GLOBALS['jio_system_is_super_admin']; }
function current_time(string $type): string { return date('Y-m-d H:i:s'); }
function add_filter(string $hook, $callback, int $priority = 10, int $args = 1): void {
$GLOBALS['aksrt_system_added'][] = ['filter', $hook, $callback, $priority];
$GLOBALS['jio_system_added'][] = ['filter', $hook, $callback, $priority];
}
function add_action(string $hook, $callback, int $priority = 10, int $args = 1): void {
$GLOBALS['aksrt_system_added'][] = ['action', $hook, $callback, $priority];
$GLOBALS['jio_system_added'][] = ['action', $hook, $callback, $priority];
}
function remove_filter(string $hook, $callback, int $priority = 10): void {
$GLOBALS['aksrt_system_removed'][] = ['filter', $hook, $callback, $priority];
$GLOBALS['jio_system_removed'][] = ['filter', $hook, $callback, $priority];
}
function remove_action(string $hook, $callback, int $priority = 10): void {
$GLOBALS['aksrt_system_removed'][] = ['action', $hook, $callback, $priority];
$GLOBALS['jio_system_removed'][] = ['action', $hook, $callback, $priority];
}
function wp_deregister_style(string $handle): void {}
function wp_register_style(string $handle, $src): void {}
function wp_enqueue_style(string $handle, $src = ''): void {}
function remove_image_size(string $name): void {
$GLOBALS['aksrt_system_added'][] = ['remove_image_size', $name, null, 10];
$GLOBALS['jio_system_added'][] = ['remove_image_size', $name, null, 10];
}
function system_tools_check(bool $condition, string $message): void {
@@ -38,14 +38,14 @@ namespace {
}
function system_tools_added(string $type, string $hook): bool {
foreach ($GLOBALS['aksrt_system_added'] as $row) {
foreach ($GLOBALS['jio_system_added'] as $row) {
if ($row[0] === $type && $row[1] === $hook) return true;
}
return false;
}
function system_tools_removed(string $type, string $hook, string $needle = ''): bool {
foreach ($GLOBALS['aksrt_system_removed'] as $row) {
foreach ($GLOBALS['jio_system_removed'] as $row) {
if ($row[0] !== $type || $row[1] !== $hook) continue;
if ($needle === '') return true;
if (is_array($row[2])) {
@@ -65,8 +65,8 @@ namespace {
}
function system_tools_reset(): void {
$GLOBALS['aksrt_system_added'] = [];
$GLOBALS['aksrt_system_removed'] = [];
$GLOBALS['jio_system_added'] = [];
$GLOBALS['jio_system_removed'] = [];
}
require dirname(__DIR__, 2) . '/src/Settings/ExtensionRuntime.php';
@@ -80,13 +80,13 @@ namespace {
require dirname(__DIR__, 2) . '/inc/helpers/template-tags/search-meta.php';
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function get_all(): array { return $GLOBALS['aksrt_system_settings']; }
public static function get_all(): array { return $GLOBALS['jio_system_settings']; }
}
}
namespace AKSRT\SystemToolsHarness {
namespace Jio\SystemToolsHarness {
final class WpEmbedStub {
public function autoembed($content) { return $content; }
public function maybe_run_ajax_cache(): void {}
@@ -94,13 +94,13 @@ namespace AKSRT\SystemToolsHarness {
}
namespace {
use AKSRT\Admin\Schema\SchemaExtensions;
use AKSRT\Admin\Schema\SchemaExtensionsSystem;
use AKSRT\Extensions\SystemTools;
use AKSRT\Settings\ExtensionRuntime;
use Jio\Admin\Schema\SchemaExtensions;
use Jio\Admin\Schema\SchemaExtensionsSystem;
use Jio\Extensions\SystemTools;
use Jio\Settings\ExtensionRuntime;
$GLOBALS['aksrt_system_is_super_admin'] = false;
$GLOBALS['wp_embed'] = new \AKSRT\SystemToolsHarness\WpEmbedStub();
$GLOBALS['jio_system_is_super_admin'] = false;
$GLOBALS['wp_embed'] = new \Jio\SystemToolsHarness\WpEmbedStub();
// 1) Defaults match the reference theme's 系统工具 defaults.
$defaults = ExtensionRuntime::get_system_tools_settings();
@@ -130,7 +130,7 @@ namespace {
}
// 2) Stored values sanitize: rename type whitelist, switch coercion, passthrough format.
$GLOBALS['aksrt_system_settings'] = [
$GLOBALS['jio_system_settings'] = [
'newfilename_type' => 'bogus',
'newfilename' => 1,
'time_ago_s' => 0,
@@ -149,18 +149,18 @@ namespace {
system_tools_check($clean['mce_float_toolbar'] === ['m_s'], 'float toolbar scenes should whitelist and dedupe');
// 2b) Unsaved float toolbar key keeps the both-scenes default; explicit empty stays empty.
$GLOBALS['aksrt_system_settings'] = ['hide_admin_bar' => 0];
$GLOBALS['jio_system_settings'] = ['hide_admin_bar' => 0];
$clean = ExtensionRuntime::get_system_tools_settings();
system_tools_check($clean['mce_float_toolbar'] === ['pc_s', 'm_s'], 'float toolbar should default to pc+mobile');
system_tools_check($clean['hide_admin_bar'] === false, 'stored falsy value should not fall back to default');
$GLOBALS['aksrt_system_settings'] = ['mce_float_toolbar' => []];
$GLOBALS['jio_system_settings'] = ['mce_float_toolbar' => []];
$clean = ExtensionRuntime::get_system_tools_settings();
system_tools_check($clean['mce_float_toolbar'] === [], 'explicitly empty float toolbar should stay empty');
// 3) Hook wiring with every switch enabled.
$GLOBALS['aksrt_system_settings'] = array_fill_keys(array_keys($expected), true);
$GLOBALS['aksrt_system_settings']['time_format'] = 'n月j日 H:i';
$GLOBALS['aksrt_system_is_super_admin'] = false;
$GLOBALS['jio_system_settings'] = array_fill_keys(array_keys($expected), true);
$GLOBALS['jio_system_settings']['time_format'] = 'n月j日 H:i';
$GLOBALS['jio_system_is_super_admin'] = false;
system_tools_reset();
SystemTools::get_instance()->init();
@@ -181,8 +181,8 @@ namespace {
system_tools_check(system_tools_removed('action', 'init', 'wp_schedule_update_checks'), 'update cron schedule should be removed');
// 4) hide_admin_bar off keeps the admin bar for super admins.
$GLOBALS['aksrt_system_settings']['hide_admin_bar'] = false;
$GLOBALS['aksrt_system_is_super_admin'] = true;
$GLOBALS['jio_system_settings']['hide_admin_bar'] = false;
$GLOBALS['jio_system_is_super_admin'] = true;
system_tools_reset();
SystemTools::get_instance()->init();
system_tools_check(!system_tools_added('filter', 'show_admin_bar'), 'super admin should keep the admin bar when hide_admin_bar is off');
@@ -225,10 +225,10 @@ namespace {
system_tools_check($passed === 'passthrough', 'update checks should pass through the blocker');
// 8) Large thumbnail pruning removes the reference theme's size list.
$GLOBALS['aksrt_system_added'] = [];
$GLOBALS['jio_system_added'] = [];
$tools->disable_large_thumbnail();
$sizes_filter = null;
foreach ($GLOBALS['aksrt_system_added'] as $row) {
foreach ($GLOBALS['jio_system_added'] as $row) {
if ($row[0] === 'filter' && $row[1] === 'intermediate_image_sizes') {
$sizes_filter = $row[2];
}
@@ -252,17 +252,17 @@ namespace {
$client_defaults = array_keys($expected);
system_tools_check(count(SchemaExtensionsSystem::build()['groups']) === 3, 'system schema should keep three groups');
// 10) aksrt_time_ago honors the 倒计时显示 switch and custom format.
// 10) jio_time_ago honors the 倒计时显示 switch and custom format.
$past = date('Y-m-d H:i:s', strtotime('-3 days'));
$GLOBALS['aksrt_system_settings'] = [];
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'relative time should be used by default');
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 0, 'time_format' => 'Y年n月j日'];
$GLOBALS['jio_system_settings'] = [];
system_tools_check(strpos(jio_time_ago($past), '天前') !== false, 'relative time should be used by default');
$GLOBALS['jio_system_settings'] = ['time_ago_s' => 0, 'time_format' => 'Y年n月j日'];
$stamp = strtotime('-3 days');
system_tools_check(aksrt_time_ago(date('Y-m-d H:i:s', $stamp)) === date('Y年n月j日', $stamp), 'custom time format should apply when countdown display is off');
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 0, 'time_format' => ''];
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'empty custom format should fall back to relative time');
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 1];
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'countdown display should stay relative when enabled');
system_tools_check(jio_time_ago(date('Y-m-d H:i:s', $stamp)) === date('Y年n月j日', $stamp), 'custom time format should apply when countdown display is off');
$GLOBALS['jio_system_settings'] = ['time_ago_s' => 0, 'time_format' => ''];
system_tools_check(strpos(jio_time_ago($past), '天前') !== false, 'empty custom format should fall back to relative time');
$GLOBALS['jio_system_settings'] = ['time_ago_s' => 1];
system_tools_check(strpos(jio_time_ago($past), '天前') !== false, 'countdown display should stay relative when enabled');
echo "System tools harness passed\n";
}
@@ -140,16 +140,16 @@ test('list margin defaults inherit the site setting while an explicit widget fal
test('multi-column frontend output uses the reference widget wrapper and carries layout variables', () => {
const wrapper = render().frontend_wrapper;
assert.match(wrapper, /class="zib-widget-wrap aksrt-tab-posts-wrap"/);
assert.match(wrapper, /data-aksrt-widget="tab_posts" data-widget-id="aksrt_widget_tab_posts-4"/);
assert.match(wrapper, /--aksrt-widget-space-top:8px/);
assert.match(wrapper, /--aksrt-widget-space-m-bottom:10px/);
assert.match(wrapper, /--aksrt-widget-bg-light:#ffffff/);
assert.match(wrapper, /--aksrt-widget-bg-dark:#111111/);
assert.match(wrapper, /class="zib-widget-wrap jio-tab-posts-wrap"/);
assert.match(wrapper, /data-jio-widget="tab_posts" data-widget-id="jio_widget_tab_posts-4"/);
assert.match(wrapper, /--jio-widget-space-top:8px/);
assert.match(wrapper, /--jio-widget-space-m-bottom:10px/);
assert.match(wrapper, /--jio-widget-bg-light:#ffffff/);
assert.match(wrapper, /--jio-widget-bg-dark:#111111/);
assert.match(wrapper, /widget-container/);
assert.match(wrapper, /class="widget-content"/);
assert.match(wrapper, /class="widget-tab-post style-mini/);
assert.doesNotMatch(wrapper, /aksrt-widget__inner container|<aside>|<\/aside>/);
assert.doesNotMatch(wrapper, /jio-widget__inner container|<aside>|<\/aside>/);
const frontend = read('src/WidgetSystem/FrontendRenderer.php');
const routeStart = frontend.indexOf("if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts', 'posts']");
@@ -59,7 +59,7 @@ class WP_Query {
}
}
namespace AKSRT\Settings {
namespace Jio\Settings {
final class Settings {
public static function get_post_list_style_settings(): array {
return $GLOBALS['tab_fixture_global_style'] ?? [];
@@ -110,7 +110,7 @@ namespace {
function wp_strip_all_tags($value): string { return trim(strip_tags((string) $value)); }
function wp_trim_words($value, int $num_words = 55, string $more = '...'): string { return implode(' ', array_slice(preg_split('/\s+/', trim((string) $value)) ?: [], 0, $num_words)) . $more; }
function wp_is_mobile(): bool { return (bool) $GLOBALS['tab_fixture_mobile']; }
function aksrt_is_show_sidebar(): bool { return true; }
function jio_is_show_sidebar(): bool { return true; }
function is_category(): bool { return false; }
function is_search(): bool { return false; }
function is_home(): bool { return false; }
@@ -130,7 +130,7 @@ namespace {
}
function get_userdata(int $id): WP_User { return new WP_User($id); }
function get_avatar_url(int $id, array $args = []): string { unset($args); return 'https://example.test/avatar/' . $id . '.png'; }
function aksrt_get_avatar_box($subject, string $class = 'avatar-img', bool $link = true, bool $vip = true): string {
function jio_get_avatar_box($subject, string $class = 'avatar-img', bool $link = true, bool $vip = true): string {
$user_id = is_numeric($subject) ? (int) $subject : 0;
$GLOBALS['tab_fixture_avatar_calls'][] = ['user_id' => $user_id, 'class' => $class, 'link' => $link, 'vip' => $vip];
$inner = '<span class="' . esc_attr($class) . '"><img class="avatar" src="https://example.test/avatar/' . $user_id . '.png" alt=""><img class="avatar-badge" src="https://example.test/vip/' . $user_id . '.svg" alt="VIP"></span>';
@@ -153,24 +153,24 @@ namespace {
function get_comments_link(int $id = 0): string { return 'https://example.test/post/' . $id . '#comments'; }
function get_post_type(int $id = 0): string { unset($id); return 'post'; }
function is_sticky(int $id = 0): bool { unset($id); return false; }
function aksrt_resource_card_badge_text(int $id): string { return isset($GLOBALS['tab_fixture_pay_meta'][$id]) ? '旧版资源标签' : ''; }
function aksrt_get_pay_meta($post = null): array {
function jio_resource_card_badge_text(int $id): string { return isset($GLOBALS['tab_fixture_pay_meta'][$id]) ? '旧版资源标签' : ''; }
function jio_get_pay_meta($post = null): array {
$post_id = $post instanceof WP_Post ? $post->ID : (int) $post;
return $GLOBALS['tab_fixture_pay_meta'][$post_id] ?? ['pay_type' => 'no'];
}
function aksrt_get_pay_types(): array { return ['reading' => '付费阅读', 'download' => '付费下载', 'gallery' => '付费图片', 'video' => '付费视频']; }
function aksrt_format_price($price): string {
function jio_get_pay_types(): array { return ['reading' => '付费阅读', 'download' => '付费下载', 'gallery' => '付费图片', 'video' => '付费视频']; }
function jio_format_price($price): string {
$value = number_format((float) $price, 2, '.', '');
return rtrim(rtrim($value, '0'), '.') ?: '0';
}
function aksrt_get_post_views(int $id): int { return $id * 10; }
function aksrt_get_post_likes(int $id): int { return $id * 2; }
function aksrt_format_number(int $number): string { return (string) $number; }
function aksrt_get_lazy_thumb(string $size = ''): string { unset($size); return 'https://example.test/placeholder.jpg'; }
function aksrt_get_lazy_attr(string $key, string $url, string $class = '', string $lazy = ''): string { unset($key, $lazy); return ' class="' . esc_attr($class) . '" src="' . esc_url($url) . '"'; }
function jio_get_post_views(int $id): int { return $id * 10; }
function jio_get_post_likes(int $id): int { return $id * 2; }
function jio_format_number(int $number): string { return (string) $number; }
function jio_get_lazy_thumb(string $size = ''): string { unset($size); return 'https://example.test/placeholder.jpg'; }
function jio_get_lazy_attr(string $key, string $url, string $class = '', string $lazy = ''): string { unset($key, $lazy); return ' class="' . esc_attr($class) . '" src="' . esc_url($url) . '"'; }
function wp_reset_postdata(): void { $GLOBALS['tab_fixture_current_post'] = null; $GLOBALS['post'] = null; }
function aksrt_widget_ajax_null(string $message = ''): string { return '<div class="aksrt-widget__empty">' . esc_html($message) . '</div>'; }
function aksrt_render_placeholder(string $style, int $count): string { return '<div data-placeholder="' . esc_attr($style) . ':' . $count . '"></div>'; }
function jio_widget_ajax_null(string $message = ''): string { return '<div class="jio-widget__empty">' . esc_html($message) . '</div>'; }
function jio_render_placeholder(string $style, int $count): string { return '<div data-placeholder="' . esc_attr($style) . ':' . $count . '"></div>'; }
function get_template_directory(): string { return dirname(__DIR__, 2); }
function get_template_directory_uri(): string { return 'https://example.test/theme'; }
function rest_url(string $path): string { return 'https://example.test/wp-json/' . $path; }
@@ -182,7 +182,7 @@ namespace {
function wp_json_encode($value): string { return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); }
require dirname(__DIR__, 2) . '/vendor/autoload.php';
require dirname(__DIR__, 2) . '/inc/helpers/aksrt-icons.php';
require dirname(__DIR__, 2) . '/inc/helpers/jio-icons.php';
require dirname(__DIR__, 2) . '/inc/helpers/posts-renderer/config.php';
require dirname(__DIR__, 2) . '/inc/helpers/posts-renderer/post-data.php';
require dirname(__DIR__, 2) . '/inc/helpers/posts-renderer/markup.php';
@@ -224,70 +224,70 @@ namespace {
if ($name === 'invalid') $instance['style'] = 'unknown';
if ($name === 'default' || $name === 'invalid') {
$cases['raw_' . $name] = [
'tab_markup' => \AKSRT\WidgetSystem\Renderers\ContentRenderer::render('tab_posts', $instance, ['widget_id' => 'aksrt_widget_tab_posts-4']),
'first' => \AKSRT\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 1, '0')['html'],
'tab_markup' => \Jio\WidgetSystem\Renderers\ContentRenderer::render('tab_posts', $instance, ['widget_id' => 'jio_widget_tab_posts-4']),
'first' => \Jio\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 1, '0')['html'],
];
}
$instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $instance);
$instance = \Jio\WidgetSystem\FieldSchema::normalize(\Jio\WidgetSystem\WidgetSchemas::for('tab_posts'), $instance);
if ($name === 'list') $list_instance = $instance;
$avatar_start = count($GLOBALS['tab_fixture_avatar_calls']);
$first_page = \AKSRT\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 1, '0');
$second_page = \AKSRT\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 2, '0');
$first_page = \Jio\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 1, '0');
$second_page = \Jio\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 2, '0');
$cases[$name] = [
'instance_style' => $instance['style'],
'tab_markup' => \AKSRT\WidgetSystem\Renderers\ContentRenderer::render('tab_posts', $instance, ['widget_id' => 'aksrt_widget_tab_posts-4']),
'tab_markup' => \Jio\WidgetSystem\Renderers\ContentRenderer::render('tab_posts', $instance, ['widget_id' => 'jio_widget_tab_posts-4']),
'first' => $first_page['html'],
'second' => $second_page['html'],
'pagination' => $first_page['paginate_html'],
'last_pagination' => \AKSRT\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 9, '0')['paginate_html'],
'last_pagination' => \Jio\WidgetSystem\PostRenderer::payload($instance, 'tab_posts', 9, '0')['paginate_html'],
'avatar_calls' => array_slice($GLOBALS['tab_fixture_avatar_calls'], $avatar_start),
];
}
$mini_instance = ['style' => 'mini', 'mini_opt' => ['show_thumb', 'show_number', 'show_meta'], 'count' => 4, 'paginate' => 'number', 'tabs' => $tabs];
$mini_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_instance);
$mini_instance = \Jio\WidgetSystem\FieldSchema::normalize(\Jio\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_instance);
$avatar_start = count($GLOBALS['tab_fixture_avatar_calls']);
$mini_first = \AKSRT\WidgetSystem\PostRenderer::payload($mini_instance, 'tab_posts', 1, '0')['html'];
$mini_second = \AKSRT\WidgetSystem\PostRenderer::payload($mini_instance, 'tab_posts', 2, '0')['html'];
$mini_first = \Jio\WidgetSystem\PostRenderer::payload($mini_instance, 'tab_posts', 1, '0')['html'];
$mini_second = \Jio\WidgetSystem\PostRenderer::payload($mini_instance, 'tab_posts', 2, '0')['html'];
$cases['mini_all_options'] = [
'first' => $mini_first,
'second' => $mini_second,
'avatar_calls' => array_slice($GLOBALS['tab_fixture_avatar_calls'], $avatar_start),
];
$cases['no_pagination_page'] = \AKSRT\WidgetSystem\PostRenderer::payload(array_merge($mini_instance, ['paginate' => '']), 'tab_posts', 2, '0');
$cases['no_pagination_page'] = \Jio\WidgetSystem\PostRenderer::payload(array_merge($mini_instance, ['paginate' => '']), 'tab_posts', 2, '0');
foreach (['list', 'card', 'mini'] as $posts_style) {
$posts_definition = \AKSRT\WidgetSystem\WidgetRegistry::get('posts');
$posts_definition = \Jio\WidgetSystem\WidgetRegistry::get('posts');
foreach (['detail', 'ajax'] as $load_mode) {
$posts_instance = $posts_definition->normalize(['style' => $posts_style, 'load_mode' => $load_mode, 'count' => 4, 'paginate' => 'number', 'title' => '文章列表']);
$cases['posts_' . $posts_style . '_' . $load_mode] = \AKSRT\WidgetSystem\Renderers\ContentRenderer::render('posts', $posts_instance, ['widget_id' => 'aksrt_widget_posts-3']);
$cases['posts_' . $posts_style . '_' . $load_mode] = \Jio\WidgetSystem\Renderers\ContentRenderer::render('posts', $posts_instance, ['widget_id' => 'jio_widget_posts-3']);
}
}
$GLOBALS['tab_fixture_global_style']['post_list_author'] = false;
$mini_time_instance = ['style' => 'mini', 'mini_opt' => ['show_meta'], 'count' => 4, 'paginate' => '', 'tabs' => $tabs];
$mini_time_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_time_instance);
$cases['mini_time_source'] = ['html' => \AKSRT\WidgetSystem\PostRenderer::payload($mini_time_instance, 'tab_posts', 1, '0')['html']];
$mini_time_instance = \Jio\WidgetSystem\FieldSchema::normalize(\Jio\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_time_instance);
$cases['mini_time_source'] = ['html' => \Jio\WidgetSystem\PostRenderer::payload($mini_time_instance, 'tab_posts', 1, '0')['html']];
$GLOBALS['tab_fixture_global_style']['post_list_author'] = true;
$ajax_instance = $instance;
$ajax_instance['style'] = 'mini';
$ajax_instance['paginate'] = 'ajax';
$ajax_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $ajax_instance);
$ajax_instance = \Jio\WidgetSystem\FieldSchema::normalize(\Jio\WidgetSystem\WidgetSchemas::for('tab_posts'), $ajax_instance);
$cases['ajax_pagination'] = [
'first' => \AKSRT\WidgetSystem\PostRenderer::payload($ajax_instance, 'tab_posts', 1, '0')['paginate_html'],
'last' => \AKSRT\WidgetSystem\PostRenderer::payload($ajax_instance, 'tab_posts', 9, '0')['paginate_html'],
'first' => \Jio\WidgetSystem\PostRenderer::payload($ajax_instance, 'tab_posts', 1, '0')['paginate_html'],
'last' => \Jio\WidgetSystem\PostRenderer::payload($ajax_instance, 'tab_posts', 9, '0')['paginate_html'],
];
$GLOBALS['tab_fixture_mobile'] = true;
$cases['mobile_pagination'] = [
'middle' => \AKSRT\WidgetSystem\PostRenderer::payload($list_instance, 'tab_posts', 5, '0')['paginate_html'],
'last' => \AKSRT\WidgetSystem\PostRenderer::payload($list_instance, 'tab_posts', 9, '0')['paginate_html'],
'middle' => \Jio\WidgetSystem\PostRenderer::payload($list_instance, 'tab_posts', 5, '0')['paginate_html'],
'last' => \Jio\WidgetSystem\PostRenderer::payload($list_instance, 'tab_posts', 9, '0')['paginate_html'],
];
$GLOBALS['tab_fixture_mobile'] = false;
$cases['context_margins'] = [
'implicit' => \aksrt_renderer_config(['type' => 'list'])['no_margin'],
'explicit_false' => \aksrt_renderer_config(['type' => 'list', 'no_margin' => false])['no_margin'],
'implicit' => \jio_renderer_config(['type' => 'list'])['no_margin'],
'explicit_false' => \jio_renderer_config(['type' => 'list', 'no_margin' => false])['no_margin'],
];
$definition = \AKSRT\WidgetSystem\WidgetRegistry::get('tab_posts');
$widget = new \AKSRT\WidgetSystem\SchemaWidget($definition);
$widget->id = 'aksrt_widget_tab_posts-4';
$wrapper_instance = \AKSRT\WidgetSystem\FieldSchema::normalize($definition->fields(), [
$definition = \Jio\WidgetSystem\WidgetRegistry::get('tab_posts');
$widget = new \Jio\WidgetSystem\SchemaWidget($definition);
$widget->id = 'jio_widget_tab_posts-4';
$wrapper_instance = \Jio\WidgetSystem\FieldSchema::normalize($definition->fields(), [
'title' => '参考标题',
'layout_pin_pc' => ['top' => 8, 'right' => 6, 'bottom' => 18, 'left' => 4],
'layout_pin_m' => ['top' => 3, 'right' => 2, 'bottom' => 10, 'left' => 2],
@@ -298,25 +298,25 @@ namespace {
'tabs' => $tabs,
]);
ob_start();
\AKSRT\WidgetSystem\FrontendRenderer::render($definition, ['before_widget' => '<aside>', 'after_widget' => '</aside>'], $wrapper_instance, $widget);
\Jio\WidgetSystem\FrontendRenderer::render($definition, ['before_widget' => '<aside>', 'after_widget' => '</aside>'], $wrapper_instance, $widget);
$cases['frontend_wrapper'] = (string) ob_get_clean();
$paid_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), [
$paid_instance = \Jio\WidgetSystem\FieldSchema::normalize(\Jio\WidgetSystem\WidgetSchemas::for('tab_posts'), [
'style' => 'mini', 'mini_opt' => ['show_meta'], 'count' => 20, 'paginate' => '',
'tabs' => [['title' => '付费徽标用例', 'cat' => '9']],
]);
$cases['pay_meta_right'] = \AKSRT\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_meta_right'] = \Jio\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$paid_instance['mini_opt'] = [];
$cases['pay_meta_hidden'] = \AKSRT\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_meta_hidden'] = \Jio\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$paid_instance['mini_opt'] = ['show_meta'];
$GLOBALS['tab_fixture_global_style']['post_list_author'] = false;
$GLOBALS['tab_fixture_global_style']['post_list_time'] = false;
$cases['pay_meta_left'] = \AKSRT\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_meta_left'] = \Jio\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$GLOBALS['tab_fixture_global_style']['post_list_author'] = true;
$GLOBALS['tab_fixture_global_style']['post_list_time'] = true;
$GLOBALS['tab_fixture_paid_settings']['display']['currency_symbol'] = '€';
$cases['pay_meta_custom_currency'] = \AKSRT\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_meta_custom_currency'] = \Jio\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$GLOBALS['tab_fixture_paid_settings']['display']['currency_symbol'] = '¥';
$GLOBALS['tab_fixture_vip_settings'] = [
@@ -326,7 +326,7 @@ namespace {
2 => ['enabled' => false, 'name' => '钻石会员', 'img_icon' => 'https://example.test/vip-2.svg'],
],
];
$cases['pay_vip1_only'] = \AKSRT\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_vip1_only'] = \Jio\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$GLOBALS['tab_fixture_vip_settings'] = [
'enabled' => true,
'levels' => [
@@ -334,7 +334,7 @@ namespace {
2 => ['enabled' => true, 'name' => '钻石会员', 'img_icon' => 'https://example.test/vip-2.svg'],
],
];
$cases['pay_vip2_only'] = \AKSRT\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_vip2_only'] = \Jio\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$GLOBALS['tab_fixture_vip_settings'] = [
'enabled' => false,
'levels' => [
@@ -342,7 +342,7 @@ namespace {
2 => ['enabled' => false, 'name' => '钻石会员', 'img_icon' => 'https://example.test/vip-2.svg'],
],
];
$cases['pay_vip_disabled'] = \AKSRT\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_empty'] = aksrt_renderer_compact_pay_badge([]);
$cases['pay_vip_disabled'] = \Jio\WidgetSystem\PostRenderer::payload($paid_instance, 'tab_posts', 1, '0')['html'];
$cases['pay_empty'] = jio_renderer_compact_pay_badge([]);
echo json_encode(['cases' => $cases], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
@@ -113,7 +113,7 @@ function postsMarkup(document, unveiled = false) {
pager.setAttribute('data-widget-items', '');
pager.setAttribute('data-widget-lazy', '');
pager.setAttribute('data-widget', 'posts');
pager.setAttribute('data-instance', 'aksrt_widget_posts-2');
pager.setAttribute('data-instance', 'jio_widget_posts-2');
pager.setAttribute('data-page', '1');
pager.setAttribute('aria-busy', 'true');
pager.innerHTML = '<div class="post_ajax_loader is-loading"><div class="posts-item placeholder">Loading</div></div>';
@@ -121,7 +121,7 @@ function postsMarkup(document, unveiled = false) {
return { root };
}
function createRuntime({ fetch, pages, posts = false, unveiled = false, restUrl = '/wp-json/aksrt/v1/widgets/', observe = false } = {}) {
function createRuntime({ fetch, pages, posts = false, unveiled = false, restUrl = '/wp-json/jio/v1/widgets/', observe = false } = {}) {
const document = new MiniDocument();
const { root: widget, nav, tabOne, tabTwo } = posts ? postsMarkup(document, unveiled) : tabMarkup(document);
document.body.appendChild(widget);
@@ -152,7 +152,7 @@ function createRuntime({ fetch, pages, posts = false, unveiled = false, restUrl
observe() {}
} : undefined;
const window = {
aksrtWidgets: config,
jioWidgets: config,
location: { href: 'https://example.test/' },
matchMedia: () => ({ matches: false }),
jQuery,
@@ -487,8 +487,8 @@ test('article widget waits for its own lazy reveal and fetches the first page on
pager.dispatch('lazybeforeunveil');
assert.deepEqual(pageRequests(runtime.requests), [1]);
const url = new URL(runtime.requests[0]);
assert.equal(url.pathname, '/wp-json/aksrt/v1/widgets/posts/items');
assert.equal(url.searchParams.get('instance'), 'aksrt_widget_posts-2');
assert.equal(url.pathname, '/wp-json/jio/v1/widgets/posts/items');
assert.equal(url.searchParams.get('instance'), 'jio_widget_posts-2');
transport.pending[0].resolve({ html: '<posts class="posts-item list ajax-item" data-post-id="first">First article</posts>', page: 1, paginate_html: '' });
await tick();
assert.ok(pager.querySelector('[data-post-id="first"]'));
@@ -526,11 +526,11 @@ test('article widget failure exposes a retry that replaces the initial skeleton'
});
test('article REST addresses preserve the route for plain WordPress permalinks', async () => {
const runtime = createRuntime({ posts: true, unveiled: true, restUrl: 'https://example.test/blog/?rest_route=/aksrt/v1/widgets/' });
const runtime = createRuntime({ posts: true, unveiled: true, restUrl: 'https://example.test/blog/?rest_route=/jio/v1/widgets/' });
const url = new URL(runtime.requests[0]);
assert.equal(url.pathname, '/blog/');
assert.equal(url.searchParams.get('rest_route'), '/aksrt/v1/widgets/posts/items');
assert.equal(url.searchParams.get('instance'), 'aksrt_widget_posts-2');
assert.equal(url.searchParams.get('rest_route'), '/jio/v1/widgets/posts/items');
assert.equal(url.searchParams.get('instance'), 'jio_widget_posts-2');
assert.equal(url.searchParams.get('page'), '1');
await tick();
assert.equal(runtime.widget.querySelector('[data-widget-items]').dataset.loaded, '1');
@@ -30,7 +30,7 @@ test('tab and single post widget titles restore reference weight while preservin
assert.equal(declarations('.widget-tab-post .item-heading.font-bold')['font-weight'], '700');
assert.equal(declarations('.widget-main-post .item-heading')['font-weight'], '500');
assert.equal(declarations('.widget-main-post .item-heading.font-bold')['font-weight'], '700');
const shell = declarations('.aksrt-tab-posts-wrap');
const shell = declarations('.jio-tab-posts-wrap');
assert.equal(shell['font-size'], '14px');
assert.equal(shell['line-height'], '1.42857143');
assert.equal(shell.color, 'var(--main-color,#4e5358)');
@@ -40,7 +40,7 @@ test('tab and single post widget titles restore reference weight while preservin
assert.equal(declarations('.widget-main-post .posts-mini-con').flex, 'auto');
assert.equal(declarations('.widget-tab-post .item-meta .meta-pay').cursor, 'auto');
assert.equal(declarations('.widget-main-post .item-meta .meta-pay').cursor, 'auto');
assert.match(css, /\.aksrt-posts-wrap :where\(a, button, input, select, textarea, \[tabindex\]\):focus-visible/);
assert.match(css, /\.jio-posts-wrap :where\(a, button, input, select, textarea, \[tabindex\]\):focus-visible/);
});
test('widget pagination restores compact reference metrics over public pagination overrides', () => {
@@ -88,7 +88,7 @@ test('modern tabs have one interaction owner and no retired tab selector or unus
const runtime = read('assets/widgets/frontend.js');
const renderer = read('src/WidgetSystem/PostRenderer.php');
assert.doesNotMatch(content, /data-toggle="tab"|data-ajax/);
assert.doesNotMatch(runtime, /aksrt-widget-tabs/);
assert.doesNotMatch(runtime, /jio-widget-tabs/);
assert.doesNotMatch(renderer, /widget_style/);
assert.match(runtime, /scope\.querySelectorAll\('\.widget-tab-post'\)\.forEach\(initTabs\)/);
});
@@ -25,7 +25,7 @@ test('ticket captcha scenes are registered in all three places', () => {
const adminSchema = read('assets/admin/src/schema/sections/permissions/index.js');
for (const scene of SCENES) {
assert.match(captcha, new RegExp(`'${scene.key}'\\s*=> __\\('${scene.label}', 'aksrt'\\)`),
assert.match(captcha, new RegExp(`'${scene.key}'\\s*=> __\\('${scene.label}', 'jio'\\)`),
`captcha scene ${scene.key} missing`);
assert.ok(permissions.includes(`'label' => '${scene.label}', 'value' => '${scene.key}'`),
`PHP permissions schema missing ${scene.key}`);
@@ -54,14 +54,14 @@ test('the ticket REST layer consumes both scenes exactly once', () => {
assert.match(rest, /const CAPTCHA_SCENE_REPLY = 'ticket_reply';/);
assert.equal((rest.match(/require_captcha\(\$request, self::CAPTCHA_SCENE_/g) || []).length, 2,
'create and reply must each verify captcha exactly once');
assert.equal((rest.match(/aksrt_require_captcha\(/g) || []).length, 1,
assert.equal((rest.match(/jio_require_captcha\(/g) || []).length, 1,
'captcha verification must funnel through one helper');
});
test('the ticket panel mounts both captcha scenes', () => {
const renderer = read('inc/helpers/user-center-renderers/profile-tickets.php');
assert.ok(renderer.includes("aksrt_render_captcha('ticket_create')"), 'create mount missing');
assert.ok(renderer.includes("aksrt_render_captcha('ticket_reply')"), 'reply mount missing');
assert.ok(renderer.includes("jio_render_captcha('ticket_create')"), 'create mount missing');
assert.ok(renderer.includes("jio_render_captcha('ticket_reply')"), 'reply mount missing');
assert.ok(!renderer.includes('after_sale'), 'renderer must not mount after-sale scenes');
const js = read('assets/js/ticket-center.js');
@@ -68,7 +68,7 @@ test('default status cannot be edited or deleted in the admin surface', () => {
const config = read('src/Ticket/TicketConfigRest.php');
assert.equal((config.match(/!empty\(\$row\['is_default'\]\)/g) || []).length, 2,
'both update and delete must honour is_default');
assert.match(config, /'aksrt_ticket_status_default'/);
assert.match(config, /'jio_ticket_status_default'/);
// 原生后台不再维护状态定义,因此不再出现 is_default 判断。
const native = read('src/Admin/Native/UsersMenusTicketsCenter.php');
@@ -78,8 +78,8 @@ test('default status cannot be edited or deleted in the admin surface', () => {
// 删除前还要确认没有工单挂在该状态/部门下(守卫在 TicketConfigRest trait 中)。
assert.match(config, /count_by_status\(\(string\) \$row\['status_key'\]\) > 0/);
assert.match(config, /count_by_type\(\$id\) > 0/);
assert.match(config, /'aksrt_ticket_status_in_use'/);
assert.match(config, /'aksrt_ticket_type_in_use'/);
assert.match(config, /'jio_ticket_status_in_use'/);
assert.match(config, /'jio_ticket_type_in_use'/);
const repo = read('src/Ticket/TicketRepository.php');
assert.match(repo, /public function count_by_status\(string \$status_key\): int/);
@@ -113,13 +113,13 @@ test('ticket numbers are unique and settings-driven', () => {
test('notifications go through the message inbox with deep links', () => {
const notifier = read('src/Ticket/TicketNotifier.php');
// 所有通知都走 Message::send(),持久化到 aksrt_notification。
assert.match(notifier, /\\AKSRT\\Messages\\Message::get_instance\(\)->send\(/);
assert.match(notifier, /class_exists\('\\AKSRT\\Messages\\Message'\)/);
// 所有通知都走 Message::send(),持久化到 jio_notification。
assert.match(notifier, /\\Jio\\Messages\\Message::get_instance\(\)->send\(/);
assert.match(notifier, /class_exists\('\\Jio\\Messages\\Message'\)/);
// 新建工单通知全体管理员,催单只通知跟进人。
assert.match(notifier, /get_users\(\['role' => 'administrator', 'fields' => 'ID', 'number' => 20\]\)/);
assert.match(notifier, /\$admin_id = \(int\) \(\$ticket\['admin_id'\] \?\? 0\);/);
// 深链分别指向后台工单页与用户中心工单位。
assert.match(notifier, /admin_url\('users\.php\?page=aksrt_tickets'\)/);
assert.match(notifier, /aksrt_get_user_center_url\('tickets'\)/);
assert.match(notifier, /admin_url\('users\.php\?page=jio_tickets'\)/);
assert.match(notifier, /jio_get_user_center_url\('tickets'\)/);
});
@@ -17,12 +17,12 @@ const css = read('assets/css/admin/tickets.css');
test('ticket native page registers no AJAX read; writes reuse REST', () => {
assert.match(menus, /use UsersMenusTicketsCenter;/);
assert.match(menus, /'users.php',[\s\S]*?'aksrt_tickets'/);
assert.doesNotMatch(menus + center, /wp_ajax_aksrt_ticket_view|ajax_ticket_view|aksrt_ticket_view/);
assert.match(menus, /'users.php',[\s\S]*?'jio_tickets'/);
assert.doesNotMatch(menus + center, /wp_ajax_jio_ticket_view|ajax_ticket_view|jio_ticket_view/);
assert.match(center, /wp_create_nonce\('wp_rest'\)/);
assert.match(center, /rest_url\('aksrt\/v1\/admin\/tickets' \./);
assert.match(center, /rest_url\('jio\/v1\/admin\/tickets' \./);
assert.match(js, /'X-WP-Nonce': config.restNonce/);
assert.doesNotMatch(menus, /admin_post_aksrt_ticket_update/);
assert.doesNotMatch(menus, /admin_post_jio_ticket_update/);
assert.doesNotMatch(center, /function (handle_ticket_update|ticket_action_\w+|ticket_redirect)/);
assert.match(menus, /count_pending_tickets\(\)/);
assert.match(menus, /TicketSettings::enabled\(\)/);
@@ -44,8 +44,8 @@ test('list and workspace are page views; only creation is a dialog on the list p
// 处理工作区本身不是弹窗;只有内部备注的输入在弹窗内。
assert.equal((detail.match(/<dialog /g) || []).length, 1);
assert.match(detail, /<dialog id="' \. esc_attr\(self::TICKET_NOTE_DIALOG\)/);
assert.match(detail, /ticket_form_open\('note', \$id, 'aksrt-ticket-dialog__frame'\)/);
assert.doesNotMatch(js + css, /aksrt-ticket-detail-dialog|aksrt-ticket-modal-open|data-aksrt-ticket-panel/);
assert.match(detail, /ticket_form_open\('note', \$id, 'jio-ticket-dialog__frame'\)/);
assert.doesNotMatch(js + css, /jio-ticket-detail-dialog|jio-ticket-modal-open|data-jio-ticket-panel/);
});
test('detail lookup is independent of the filtered list; paging clamps and preserves filters', () => {
@@ -64,25 +64,25 @@ test('detail lookup is independent of the filtered list; paging clamps and prese
test('conversation, private notes and final-state confirmation remain distinct', () => {
assert.match(detail, /TicketNoteService::for_ticket\(\$id\)/);
assert.match(detail, /TicketRepository::get_instance\(\)->replies\(\$id\)/);
assert.match(detail, /!empty\(\$quote\['is_deleted'\]\) \? __\('原回复已删除', 'aksrt'\)/);
assert.match(detail, /!empty\(\$quote\['is_deleted'\]\) \? __\('原回复已删除', 'jio'\)/);
// 回复框是会话面板的底部,而不是独立卡片。
assert.match(detail, /return \$html \. '<\/ol><\/div>' \. \$footer \. '<\/section>';/);
assert.ok(detail.includes("ticket_form_open('reply', $id, 'aksrt-ticket-composer')"));
assert.match(detail, /data-aksrt-ticket-send-shortcut/);
assert.ok(detail.includes("ticket_form_open('reply', $id, 'jio-ticket-composer')"));
assert.match(detail, /data-jio-ticket-send-shortcut/);
assert.match(js, /event\.ctrlKey \|\| event\.metaKey/);
assert.ok(detail.includes("ticket_form_open('status'"));
assert.doesNotMatch(detail, /function ticket_composer\(/);
assert.match(detail, /aksrt-ticket-detail__closed/);
assert.match(detail, /jio-ticket-detail__closed/);
assert.match(detail, /TicketStatusService::can_transition\(\$from, \$key\)/);
assert.match(detail, /data-aksrt-ticket-close-confirm/);
assert.match(detail, /data-aksrt-ticket-assign/);
assert.match(detail, /data-jio-ticket-close-confirm/);
assert.match(detail, /data-jio-ticket-assign/);
assert.match(detail, /仅管理员可见,不会发送给用户/);
});
test('page safety: unsaved-change guard, drafts survive the post-save reload, uncertain writes lock forms', () => {
assert.match(js, /window.addEventListener\('beforeunload'/);
assert.match(js, /sessionStorage/);
assert.match(js, /aksrt_ticket_msg/);
assert.match(js, /jio_ticket_msg/);
assert.match(js, /next.length > field.maxLength/);
assert.match(js, /strings.uncertain/);
assert.match(js, /\[401, 403, 404, 409\]/);
@@ -94,19 +94,19 @@ test('page safety: unsaved-change guard, drafts survive the post-save reload, un
});
test('configuration remains in the SPA; native ops has no old config tabs', () => {
assert.match(list, /admin_url\('admin\.php\?page=aksrt-settings#\/users\/ticket'\)/);
assert.match(list, /admin_url\('admin\.php\?page=jio-settings#\/users\/ticket'\)/);
assert.doesNotMatch(center + list + detail, /nav-tab-wrapper|ticket_tab|UsersMenusTicketsConfig/);
assert.ok(!fs.existsSync(path.join(root, 'src/Admin/Native/UsersMenusTicketsConfig.php')));
assert.ok(read('assets/admin/src/router/index.js').includes("{ path: '/users/tickets', title: '工单管理', nativeAdminPath: 'users.php?page=aksrt_tickets' }"));
assert.ok(read('assets/admin/src/router/index.js').includes("{ path: '/users/tickets', title: '工单管理', nativeAdminPath: 'users.php?page=jio_tickets' }"));
});
test('frontend hooks remain aligned with their independent REST consumer', () => {
const renderer = read('inc/helpers/user-center-renderers/profile-tickets.php');
const frontend = read('assets/js/ticket-center.js') + '\n' + read('assets/js/ticket-center-core.js');
for (const hook of ['data-aksrt-ticket-center', 'data-aksrt-ticket-create', 'data-aksrt-ticket-stats', 'data-aksrt-ticket-stat=', 'data-aksrt-ticket-list', 'data-aksrt-ticket-pagination', 'data-aksrt-tickets-search', 'data-aksrt-ticket-filter=']) {
for (const hook of ['data-jio-ticket-center', 'data-jio-ticket-create', 'data-jio-ticket-stats', 'data-jio-ticket-stat=', 'data-jio-ticket-list', 'data-jio-ticket-pagination', 'data-jio-tickets-search', 'data-jio-ticket-filter=']) {
assert.ok(renderer.includes(hook), `renderer missing ${hook}`);
}
for (const hook of ['data-aksrt-ticket-center', 'data-aksrt-ticket-create', 'data-aksrt-ticket-list', 'data-aksrt-ticket-view=', 'data-aksrt-ticket-page=', 'data-aksrt-ticket-quote=', 'data-aksrt-ticket-images']) {
for (const hook of ['data-jio-ticket-center', 'data-jio-ticket-create', 'data-jio-ticket-list', 'data-jio-ticket-view=', 'data-jio-ticket-page=', 'data-jio-ticket-quote=', 'data-jio-ticket-images']) {
assert.ok(frontend.includes(hook), `frontend missing ${hook}`);
}
for (const endpoint of ["'/tickets'", "'/tickets/statistic'", "'/tickets/meta'", "'/tickets/' + ticketId", "'/tickets/' + ticket.id + '/reply'", "'/ticket/upload-attachment'"]) {
@@ -115,5 +115,5 @@ test('frontend hooks remain aligned with their independent REST consumer', () =>
assert.match(frontend, /runCaptcha\('ticket_create'\)/);
assert.match(frontend, /runCaptcha\('ticket_reply'\)/);
const routeCss = read('assets/css/routes/tickets.css');
for (const hook of ['.aksrt-tickets-page', '.aksrt-ticket-badge', '.aksrt-ticket-detail', '--aksrt-ticket-color']) assert.ok(routeCss.includes(hook));
for (const hook of ['.jio-tickets-page', '.jio-ticket-badge', '.jio-ticket-detail', '--jio-ticket-color']) assert.ok(routeCss.includes(hook));
});
@@ -88,7 +88,7 @@ test('linked orders must belong to the caller and be paid', () => {
assert.match(body, /get_user_order\(get_current_user_id\(\), \$order_number\)/);
assert.match(body, /ORDER_STATUS_PAID/);
// 新建时关联订单非法必须拒掉。
assert.match(rest, /if \(\$order_number !== '' && !\$this->owns_paid_order\(\$order_number\)\) \{\s*\n\s*return new \\WP_Error\('aksrt_ticket_order'/);
assert.match(rest, /if \(\$order_number !== '' && !\$this->owns_paid_order\(\$order_number\)\) \{\s*\n\s*return new \\WP_Error\('jio_ticket_order'/);
});
test('admin routes all require manage_options', () => {
@@ -104,7 +104,7 @@ test('admin routes all require manage_options', () => {
assert.match(admin, /current_user_can\('manage_options'\)/);
// 管理侧命名空间与用户侧隔离。
assert.match(body, /\$namespace = 'aksrt\/v1\/admin';/);
assert.match(body, /\$namespace = 'jio\/v1\/admin';/);
});
test('closed tickets refuse replies and cannot be re-closed', () => {
@@ -112,10 +112,10 @@ test('closed tickets refuse replies and cannot be re-closed', () => {
const admin = read('src/Ticket/TicketRestAdmin.php');
// 用户侧
assert.match(rest, /if \(!empty\(\$ticket\['is_closed'\]\)\) \{\s*\n\s*return new \\WP_Error\('aksrt_ticket_closed', __\('工单已关闭,无法回复'/);
assert.match(rest, /if \(!empty\(\$ticket\['is_closed'\]\)\) \{\s*\n\s*return new \\WP_Error\('aksrt_ticket_closed', __\('工单已关闭,无法催单'/);
assert.match(rest, /if \(!empty\(\$ticket\['is_closed'\]\)\) \{\s*\n\s*return new \\WP_Error\('jio_ticket_closed', __\('工单已关闭,无法回复'/);
assert.match(rest, /if \(!empty\(\$ticket\['is_closed'\]\)\) \{\s*\n\s*return new \\WP_Error\('jio_ticket_closed', __\('工单已关闭,无法催单'/);
// 管理侧
assert.match(admin, /if \(!empty\(\$ticket\['is_closed'\]\)\) \{\s*\n\s*return new \\WP_Error\('aksrt_ticket_closed'/);
assert.match(admin, /if \(!empty\(\$ticket\['is_closed'\]\)\) \{\s*\n\s*return new \\WP_Error\('jio_ticket_closed'/);
// close_ticket 自身也挡已完结(双保险,避免前端绕过 UI 直接 POST)。
const status = read('src/Ticket/TicketStatus.php');
@@ -127,9 +127,9 @@ test('urge has a cooldown driven by settings', () => {
const body = rest.slice(rest.indexOf('public function urge('));
// 冷却以工单行 last_urge_time 为准(条件 UPDATE 防并发),不再依赖 transient。
assert.ok(!/_transient\('aksrt_ticket_urge_/.test(rest), 'urge cooldown must not use transients');
assert.ok(!/_transient\('jio_ticket_urge_/.test(rest), 'urge cooldown must not use transients');
assert.match(body, /\$count = \$repo->record_urge\(\$id, TicketSettings::urge_interval\(\) \* MINUTE_IN_SECONDS\);/);
assert.match(body, /if \(\$count <= 0\) \{\s*\n\s*return new \\WP_Error\('aksrt_ticket_urge_limit'/);
assert.match(body, /if \(\$count <= 0\) \{\s*\n\s*return new \\WP_Error\('jio_ticket_urge_limit'/);
const repo = read('src/Ticket/TicketRepository.php');
assert.match(repo, /SET urge_count = urge_count \+ 1, last_urge_time = %s/);
@@ -142,7 +142,7 @@ test('urge writes a system record, notifies admins and fires the mail hook', ()
assert.match(body, /\$repo->create_reply\(\$id, 'system', get_current_user_id\(\)/);
assert.match(body, /TicketNotifier::admin_activity\(\$ticket, 'urge'\);/);
assert.match(body, /do_action\('aksrt_ticket_urged', \$ticket\);/);
assert.match(body, /do_action\('jio_ticket_urged', \$ticket\);/);
const repo = read('src/Ticket/TicketRepository.php');
assert.match(repo, /if \(!in_array\(\$author_type, \['admin', 'system'\], true\)\) \{/);
Loaded 100 of 997 files, more files were not shown because too many files have changed in this diff. Show more