const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); test('message center exposes only the four configured-alias parameter-free routes', () => { const routing = read('inc/modules/user-parts/routing.php'); 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(routing, /add_rewrite_rule\('\^' \. \$message_slug \. '\/\(posts\|like\|system\)\/\?\$'/); assert.match(routing, /message-routing:' \. aksrt_get_messages_rewrite_slug\(\)/); assert.doesNotMatch(routing, /add_rewrite_rule\('\^message/); assert.doesNotMatch(routing, /message\/\(\[0-9]|aksrt_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')), ); assert.match(messageUrlFunction, /aksrt_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 ''/); assert.match(routing, /get_page_template_slug[\s\S]*?page-messages\.php[\s\S]*?set_404/); }); test('message center URL helper builds every view from the configured alias', () => { const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'message-center-url-harness.php')], { cwd: root, encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr || result.stdout); assert.deepEqual(JSON.parse(result.stdout), { slug: 'inbox-center', unread: 'https://example.test/inbox-center/', posts: 'https://example.test/inbox-center/posts/', like: 'https://example.test/inbox-center/like/', system: 'https://example.test/inbox-center/system/', }); }); test('message pagination setting switches between cumulative loading and numeric replacement', () => { const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'message-center-pagination-harness.php')], { cwd: root, encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr || result.stdout); assert.deepEqual(JSON.parse(result.stdout), { ajax_items: 40, ajax_has_more: true, ajax_has_numbers: false, default_items: 20, default_has_more: false, default_numbers: true, default_current_2: true, }); const script = read('assets/js/messages.js'); const styles = read('assets/css/frontend/messages.css'); assert.match(script, /data-message-next-page|dataset\.messageNextPage/); assert.match(script, /appendMessagePage\(result\.data\.content\)/); assert.match(styles, /\.messages-pagination__more/); assert.match(styles, /\.messages-pagination__button\.is-current/); }); test('message centre shows a skeleton during the AJAX load-more cycle', () => { const list = read('inc/modules/message-center.php'); const placeholder = read('inc/helpers/message-placeholder.php'); const script = read('assets/js/messages.js'); const styles = read('assets/css/frontend/messages.css'); // 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(placeholder, /message-item--placeholder/); assert.match(placeholder, /class="post_ajax_loader"/); // The message skeleton reuses the shared .placeholder shimmer contract. assert.match(placeholder, /placeholder t1/); assert.match(placeholder, /placeholder k2/); assert.match(placeholder, /placeholder s1/); // Load cycle: show on start, hide on finish, and never remove the node. assert.match(script, /content\.querySelector\('\.messages-list > \.post_ajax_loader'\)/); assert.match(script, /skeleton\.classList\.add\('is-loading'\)/); assert.match(script, /loader\.classList\.remove\('is-loading'\)/); // Appended rows land before the skeleton so it stays flush under the list. assert.match(script, /currentList\.insertBefore\(item, skeleton\)/); // Visibility is class-driven; the old button spinner / opacity dim are gone. assert.match(styles, /\.messages-list > \.post_ajax_loader \{/); assert.match(styles, /\.messages-list > \.post_ajax_loader\.is-loading \{/); assert.doesNotMatch(styles, /messages-pagination-spin/); assert.doesNotMatch(styles, /opacity: 0\.55/); }); test('tabs, pagination, actions, and detail all replace DOM fragments', () => { const center = read('inc/modules/message-center.php'); const script = read('assets/js/messages.js'); const template = read('templates/pages/page-messages.php'); 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(template, /data-message-content/); assert.match(script, /nav\.innerHTML = result\.data\.nav/); assert.match(script, /content\.innerHTML = result\.data\.content/); assert.match(script, /\.js-msg-detail/); assert.match(script, /data-message-back/); assert.doesNotMatch(script, /searchParams|msg_type|msg_page|location\.href\s*=/); assert.doesNotMatch(script, /pushState\([^\n]+detail/); }); 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 detail = center.slice(detailStart, detailEnd); assert.doesNotMatch(detail, /aksrt_notification_type_label/); assert.doesNotMatch(detail, /查看相关内容|message-detail__primary|target_url|\['url'\]/); assert.doesNotMatch(detail, / { const notifications = read('inc/modules/user-notifications.php'); const settings = read('src/Settings/Integration.php'); const adminSettings = read('assets/admin/src/pages/content/NotificationCenterSettings.vue'); const combined = [notifications, settings, adminSettings].join('\n'); for (const retired of [ 'asset_points_increase', 'asset_points_decrease', 'asset_balance_increase', 'asset_balance_decrease', ]) { 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.ok(notifications.includes(`add_action('${hook}'`)); } }); test('notification storage has no message-center legacy metadata fallback', () => { const notifications = read('inc/modules/user-notifications.php'); const service = read('src/Messages/Message.php'); const combined = `${notifications}\n${service}`; assert.doesNotMatch(combined, /aksrt_notifications|aksrt_unread_message_count/); assert.doesNotMatch(combined, /legacy|兼容|降级/i); });