Files
wordpress-theme-aksrt/tests/admin/pay-pages-pagination.test.cjs
T
kate522 afd08885fc fix(admin): 支付中心分页条改回四按钮式
AdminListPagination 的输出标记从 WP 原生 paginate_links() 换回「卡密管理」
原版的四按钮式:`« ‹ 第 X 页,共 Y 页 › »` +「每页 X 共Y」计数,没有数字页码
链接。七个页面调用点不变,外观统一为卡密管理那一套。

- 删除 $total_pages <= 1 早退:只要总数 > 0 就渲染分页条,单页时四枚按钮全
  disabled —— 与原版 render_pagenavi 行为一致
- 基准 URL 与页码夹取沿用现有实现(显式拼装、夹到 [1, total_pages]),
  这两处缺陷修复不动
- 标题属性由 esc_attr_e() 改为 esc_attr__():输出相同,且同文件与 dev harness
  都只用后者
- 新增私有静态 print_assets(),由 render() 调用并以 $assets_printed 保证每页
  只输出一次;内容只有 .tablenav-pages a.disabled { pointer-events: none; }
  及其注释。这些原生后台页面不加载主题 CSS,故随标记内联

清理 harness 中已无人调用的 number_format_i18n() 与 paginate_links() stub。

验证:tests/admin/ 76 项通过;tests/widget-system/ 458/459(仅剩与本次无关的
lazyload-contract 既有失败);用 git show HEAD~1 取出原 render_pagenavi() 与
新助手在 8 组输入(含单页、总数为 0、per_page 为 0、页码越界)下比对,规范化
空白后输出逐字节一致;harness 渲染售后面板确认四按钮标记与内联样式就位。
2026-10-09 11:49:16 +08:00

231 lines
11 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';
/**
* 支付中心列表页分页契约(四按钮式,统一到 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(),不再有各自的翻页实现;
* - 渲染标记是卡密管理那一套:« ‹ 第 X 页,共 Y 页 › » 四枚按钮 + 「每页 X 共Y」计数,
* 没有数字页码链接;只要总数 > 0 就渲染,单页时四枚按钮全 disabled;
* - 每页条数默认 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;/);
// 总数为 0 或每页条数非法时不渲染;单页照样渲染(与原版 render_pagenavi 一致)。
assert.match(helper, /if \(\$total_count <= 0 \|\| \$per_page <= 0\) \{\s*return;\s*\}/);
assert.doesNotMatch(helper, /\$total_pages <= 1/, '单页也必须渲染分页条');
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\);/);
// 基准 URL 显式拼装:add_query_arg 必带第三个参数(两参数写法会退回 REQUEST_URI)。
assert.match(helper, /\$base\s*=\s*add_query_arg\(array_merge\(\['page' => \$page_slug\], \$query_args\), admin_url\('admin\.php'\)\);/);
assert.match(helper, /\$page_url = static function \(int \$page\) use \(\$base\): string \{/);
assert.match(helper, /return add_query_arg\('paged', \$page, \$base\);/);
assert.doesNotMatch(helper, /add_query_arg\('paged',\s*'[^']*'\s*\)/);
// 卡密管理式四按钮标记。
assert.match(helper, /<div class="tablenav bottom">/);
assert.match(helper, /<div class="tablenav-pages">/);
assert.match(helper, /<span class="displaying-num">/);
assert.match(helper, /每页 %1\$s 共%2\$s/);
assert.match(helper, /<span class="pagination-links">/);
assert.match(helper, /<span class="paging-input">/);
assert.match(helper, /第 %1\$s 页,共 %2\$s 页/);
assert.match(helper, /<span class="total-pages">/);
for (const cls of ['first-page', 'prev-page', 'next-page', 'last-page']) {
assert.match(helper, new RegExp(`class="${cls} button`), `缺少 ${cls} 按钮`);
}
assert.match(helper, /\$on_first = \$current_page <= 1;/);
assert.match(helper, /\$on_last\s*=\s*\$current_page >= \$total_pages;/);
assert.equal(count(helper, /\$on_first \? '#' :/g), 2, '第一页/上一页都要按 on_first 落到 #');
assert.equal(count(helper, /\$on_last \? '#' :/g), 2, '下一页/最后一页都要按 on_last 落到 #');
// 禁用的翻页按钮不可点击,否则 <a href="#"> 会把页面滚回顶部。
assert.match(helper, /\.tablenav-pages a\.disabled \{ pointer-events: none; \}/);
assert.match(helper, /private static \$assets_printed = false;/);
assert.match(helper, /private static function print_assets\(\): void \{/);
// 不再使用 WP 原生 paginate_links。
assert.doesNotMatch(helper, /echo paginate_links\(/);
});