Files
wordpress-theme-aksrt/tests/admin/pay-pages-pagination.test.cjs
T
kate522 7b83893cf6 feat(admin): 支付中心列表页统一为 WP 原生分页
七个支付中心列表页(订单明细 / 售后服务 / 付费文章 / 优惠码 / 创作分成 /
提现审核 / 卡密管理)的分页收敛到共享助手 AdminListPagination,改用 WP 原生
paginate_links() 渲染。

- 基准 URL 显式以 page 参数 + 当前筛选参数拼装,不再让两个参数的
  add_query_arg('paged', ...) 退回 $_SERVER['REQUEST_URI']:链接不再随当前
  请求形态漂移,列条件也不再丢失
- 售后服务从写死的 limit => 100 改为 limit/offset 分页,总数接上原本零调用的
  AfterSaleService::count();after_sale_status 先按状态常量归一,保证 count()
  与 admin_list() 口径一致,分页条不说谎
- 每页 20 条,统一来自 AdminListPagination::DEFAULT_PER_PAGE
- 每页在查询前夹取页码,越界时回退到最后一页并重查,杜绝「空表且没有分页条」
  的死胡同;页码夹取与早退也一并收敛到助手
- 提现审核的翻页链接排除 retry_id / retry_key 两个一次性动作参数
- 订单明细的标签页与翻页链接共用同一份筛选参数,不再各写一份

清理死代码:五个页面里近乎复制的 render_pagination()(其中订单明细与提现审核
两份从未被调用)、两页重复的内联 paginate_links 块、卡密管理的 render_pagenavi()
及其 docblock、PayChargeCards::DEFAULT_PER_PAGE 常量,以及只服务自定义禁用
按钮的 .tablenav-pages a.disabled 规则。

验证:tests/admin/ 76 项通过(新增 pay-pages-pagination.test.cjs,重写
charge-cards-pagination.test.cjs);用 git show HEAD 取出原文件对比,订单明细
与售后服务两页在新旧实现下渲染输出逐字节相同。
2026-10-08 22:22:08 +08:00

211 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* 支付中心列表页分页契约(WP 原生 paginate_links,统一到 AdminListPagination)。
*
* 背景(本次收敛的缺陷):
* 1) 各页面的翻页基准 URL 写成 add_query_arg('paged', '%#%') —— 只有两个参数时
* WordPress 会落到 $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态漂移,
* 列条件也容易丢。现在统一由 AdminListPagination 以 page 参数 + 当前筛选参数显式拼装。
* 2) 五个页面各复制一份几乎相同的 render_pagination(),其中「订单明细」与「提现审核」
* 的那份从未被调用(死代码),两页各自又内联了一份 paginate_links。
* 3) 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
* 且分页条不输出,只能手改 URL 退出。现在每页回退到最后一页重查一次。
* 4) 「售后服务」原本写死 limit => 100,完全没有分页。
*
* 本契约固化:
* - 七个页面全部改走 AdminListPagination::render(),不再有各自的翻页实现;
* - 每页条数默认 20 且来自共享常量,不新增「每页显示」控件;
* - 每页都在查询前夹取页码,越界时重查,杜绝「空表且没有分页条」的死胡同;
* - 翻页链接显式带上当前筛选参数,不依赖 REQUEST_URI。
*/
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
const helper = read('src/Admin/Native/AdminListPagination.php');
const pages = {
orders: read('src/Admin/Native/PayOrders.php'),
products: read('src/Admin/Native/PayProducts.php'),
coupons: read('src/Admin/Native/PayCoupons.php'),
income: read('src/Admin/Native/PayIncome.php'),
withdrawals: read('src/Admin/Native/PayWithdrawals.php'),
chargeCards: read('src/Admin/Native/PayChargeCards.php'),
};
const labels = {
orders: '订单明细',
products: '付费文章',
coupons: '优惠码',
income: '创作分成',
withdrawals: '提现审核',
chargeCards: '卡密管理',
};
const count = (src, pattern) => (src.match(pattern) || []).length;
const emptyStates = {
orders: '暂无订单',
products: '暂无付费文章',
coupons: '暂无优惠码',
income: '暂无分成记录',
withdrawals: '暂无提现记录',
chargeCards: '未找到对应内容',
};
test('七个页面全部改走共享分页助手', () => {
for (const [key, src] of Object.entries(pages)) {
assert.ok(
src.includes('AdminListPagination::render('),
`${labels[key]} 未使用 AdminListPagination::render()`
);
}
// 订单明细 + 售后服务在同一个文件里,各有一处。
assert.equal(count(pages.orders, /AdminListPagination::render\(/g), 2, '订单明细与售后服务应各有一处');
for (const key of Object.keys(pages)) {
if (key === 'orders') continue;
assert.equal(count(pages[key], /AdminListPagination::render\(/g), 1, `${labels[key]} 只应有一处`);
}
});
test('不再有各页面自己的翻页实现,也没有 REQUEST_URI 基准 URL', () => {
// 只查六个页面:共享助手的 docblock 会引用这些反例,不能把它算进来。
for (const [key, src] of Object.entries(pages)) {
const label = labels[key];
assert.doesNotMatch(src, /private function render_pagenavi\(/, `${label} 仍保留私有 render_pagenavi()`);
assert.doesNotMatch(src, /private function render_pagination\(/, `${label} 仍保留私有 render_pagination()`);
assert.doesNotMatch(src, /public const DEFAULT_PER_PAGE/, `${label} 仍保留自己的每页条数常量`);
assert.doesNotMatch(src, /self::DEFAULT_PER_PAGE/, `${label} 仍引用自己的每页条数常量`);
// 两个参数的 add_query_arg('paged', ...) 会退回 $_SERVER['REQUEST_URI']。
assert.doesNotMatch(src, /add_query_arg\('paged',\s*'[^']*'\s*\)/, `${label} 仍以 REQUEST_URI 为基准`);
assert.doesNotMatch(src, /echo paginate_links\(/, `${label} 仍直接调用 paginate_links`);
}
});
test('每页条数默认 20 来自共享常量,且无「每页显示」控件', () => {
assert.match(helper, /public const DEFAULT_PER_PAGE = 20;/);
for (const [key, src] of Object.entries(pages)) {
assert.ok(
src.includes('AdminListPagination::DEFAULT_PER_PAGE'),
`${labels[key]} 未使用共享每页条数`
);
}
// 明确约定:任何页面都不新增「每页显示」下拉/按钮。
for (const [key, src] of Object.entries(pages)) {
assert.doesNotMatch(src, /per_page_selector/, `${labels[key]} 出现 per_page 选择器`);
assert.doesNotMatch(src, /PER_PAGE_CHOICES/, `${labels[key]} 出现 PER_PAGE_CHOICES`);
assert.doesNotMatch(src, /aksrt-perpage-form/, `${labels[key]} 出现每页条数表单`);
assert.doesNotMatch(src, /name="ice_perpage"/, `${labels[key]} 出现 ice_perpage 输入框`);
assert.doesNotMatch(src, /order-header-right/, `${labels[key]} 出现工具栏右侧每页控件`);
}
});
test('每页都在查询前夹取页码,越界时重查一次', () => {
// 六个页面统一按 $page_num / $pages 夹取;卡密管理沿用自己既有的 $paged / $max_page 回退。
for (const key of ['orders', 'products', 'coupons', 'income', 'withdrawals']) {
assert.match(pages[key], /if \(\$page_num > \$pages\) \{/, `${labels[key]} 缺少页码夹取`);
}
assert.match(
pages.chargeCards,
/if \(!\$items && \$count_all > 0 && \$paged > \$max_page\)/,
'卡密管理缺少越界回退'
);
const requeries = {
orders: [/\$items\s*=\s*\$repo->get_orders_admin\(/g, /\$items\s*=\s*\$service->admin_list\(/g],
products: [/\$query\s*=\s*\$this->query_paid_posts\(/g],
coupons: [/\$svc->get_list\(/g],
income: [/\$svc->get_list\(/g],
withdrawals: [/\$svc->get_all\(/g],
chargeCards: [/\$svc->get_list\(/g],
};
for (const [key, patterns] of Object.entries(requeries)) {
for (const pattern of patterns) {
assert.equal(count(pages[key], pattern), 2, `${labels[key]} 越界后必须重查(${pattern})`);
}
}
});
test('售后服务改为真正的 limit/offset 分页', () => {
assert.doesNotMatch(pages.orders, /limit => 100/, '售后服务仍写死 limit => 100');
assert.match(pages.orders, /\$total = \$service->count\(\$filter_args\);/, '售后服务未用 AfterSaleService::count() 取总数');
// count() 不像 admin_list() 那样校验状态,查询前必须先归一,否则分页条会说谎。
assert.match(pages.orders, /if \(!in_array\(\$status, \$this->after_sale_statuses\(\), true\)\)/);
assert.match(pages.orders, /private function after_sale_statuses\(\): array \{/);
});
test('分页条晚于空态分支渲染', () => {
for (const [key, src] of Object.entries(pages)) {
const marker = emptyStates[key];
assert.ok(src.includes(marker), `${labels[key]} 找不到空态标记 ${marker}`);
if (key === 'orders') {
// 订单明细是第一处,售后服务是最后一处。
assert.ok(
src.indexOf('AdminListPagination::render(') > src.indexOf(marker),
'订单明细的分页条必须晚于空态分支'
);
assert.ok(
src.lastIndexOf('AdminListPagination::render(') > src.indexOf('暂无售后申请'),
'售后服务的分页条必须晚于空态分支'
);
continue;
}
assert.ok(
src.indexOf('AdminListPagination::render(') > src.indexOf(marker),
`${labels[key]} 的分页条必须晚于空态分支`
);
}
});
test('提现审核不把一次性动作参数带进翻页链接', () => {
// 只取 $query_args 数组本体:上下文的注释与行循环里的 $retry_id 用法都不算。
const start = pages.withdrawals.indexOf('$query_args = array_filter([');
assert.ok(start !== -1, '提现审核缺少 $query_args 白名单');
const slice = pages.withdrawals.slice(
start,
pages.withdrawals.indexOf('], static function', start)
);
assert.doesNotMatch(slice, /retry_id/, 'retry_id 不得进翻页链接');
assert.doesNotMatch(slice, /retry_key/, 'retry_key 不得进翻页链接');
for (const key of ['status', 's', 'user_id', 'date_from', 'date_to']) {
assert.match(slice, new RegExp(`'${key}'\\s*=>`), `缺少筛选参数 ${key}`);
}
});
test('共享助手自身:页码夹取、早退与显式基准 URL', () => {
assert.match(helper, /namespace AKSRT\\Admin\\Native;/);
assert.match(helper, /defined\('ABSPATH'\) \|\| exit;/);
// 空态与单页都不渲染。
assert.match(helper, /if \(\$total_count <= 0 \|\| \$per_page <= 0\) \{\s*return;\s*\}/);
assert.match(helper, /\$total_pages\s*=\s*max\(1, \(int\) ceil\(\$total_count \/ \$per_page\)\);/);
assert.match(helper, /\$current_page = min\(max\(1, \$current_page\), \$total_pages\);/);
assert.match(helper, /if \(\$total_pages <= 1\) \{\s*return;\s*\}/);
// 基准 URL 显式拼装:add_query_arg 必带第三个参数(那正是两参数写法退回 REQUEST_URI 的根治点)。
assert.match(helper, /\$base = add_query_arg\(array_merge\(\['page' => \$page_slug\], \$query_args\), admin_url\('admin\.php'\)\);/);
assert.match(helper, /\$base = add_query_arg\('paged', '%#%', \$base\);/);
// 原生 WP 分页 + 总数提示。
assert.match(helper, /<div class="tablenav"><div class="tablenav-pages">/);
assert.match(helper, /<span class="displaying-num">/);
assert.match(helper, /number_format_i18n\(\$total_count\)/);
assert.match(helper, /echo paginate_links\(\[/);
assert.match(helper, /'prev_text' => '&laquo;',/);
assert.match(helper, /'next_text' => '&raquo;',/);
assert.match(helper, /phpcs:ignore WordPress\.Security\.EscapeOutput -- paginate_links 自行转义。/);
});