Files
wordpress-theme-aksrt/tests/admin/charge-cards-pagination.test.cjs
T
kate522 0e7b888277 fix(payment): 修复卡密管理分页:页码夹取、显式基准 URL、补齐每页条数入口
- render_pagenavi():页码先夹到 [1, total_pages],禁用状态由 $on_first / $on_last
  一次导出。旧写法是一串 if/elseif,页码越界时三个分支一个都不命中,
  上一页与下一页同时停在 '#':既不打 disabled 也点不动。
- 翻页链接改用显式基准 URL(page_url + 当前筛选参数),不再依赖
  add_query_arg() 默认取 $_SERVER['REQUEST_URI'] 的行为;
  status / type / other / s / orderby / desc / ice_perpage 随链接带走。
- render_list():页码越界(手改 URL、或筛选后总数变小)时回退到最后一页重查,
  消除「空白表格 + 一个分页控件都没有」的死胡同。
- 新增「每页显示」选择器(ice_perpage)。该参数此前只有服务端读取、界面上没有
  任何入口能改,条数小于默认每页条数时永远只有一页,分页条恒为 disabled。
- 禁用按钮加 pointer-events:none,避免点空还滚回页首。
- 新增 tests/admin/charge-cards-pagination.test.cjs(6 条契约)。

验证:容器内真实渲染 7 组 URL(默认 / 每页 5 / 第 2 页 / paged=2 越界 /
paged=99 越界 / 带 status 筛选 / 非预设每页 7),无头 Chrome 截图核对版式;
node --test 三套 18/18 通过;负向对照(还原旧写法)按要求失败 2 条;php -l 通过。
2026-09-17 12:45:22 +08:00

110 lines
6.0 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';
/**
* 「卡密管理」分页契约。
*
* 背景(已修复的缺陷):
* 1) 翻页链接此前写作 add_query_arg('paged', N) —— 只有两个参数时 WordPress 会落到
* $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态漂移,列条件也容易丢。
* 2) render_pagenavi() 的 URL 推导是一串 if/elseif。页码越界时(例如 per_page 缩小后
* URL 里还留着 paged=2,而总数只剩 1 页)三个分支一个都不命中,上一页/下一页
* 同时停在 '#':按钮既不打 disabled 也点不动,还停在「第 1 页」状态无法自洽。
* 极端情况下 offset 落到数据之外,表格直接变空态并且连分页条都不输出,只能手改 URL 退出。
* 3) ice_perpage 只在服务端被读取,界面上没有任何入口能改每页条数。条数小于默认每页条数时
* 永远只有一页,四个按钮恒为 disabled —— 看上去就是「分页是坏的」。
*
* 本契约固化三件事:
* - 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导;
* - 翻页链接基于显式基准 URL(page_url + 当前筛选参数),不再依赖 REQUEST_URI;
* - 列表页提供「每页显示」选择器(name=ice_perpage),并把筛选参数带进翻页链接。
*/
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 page = fs.readFileSync(path.join(root, 'src/Admin/Native/PayChargeCards.php'), 'utf8');
const count = (src, pattern) => (src.match(pattern) || []).length;
const slice = (from, to) => {
const start = page.indexOf(from);
const end = page.indexOf(to);
if (start === -1 || end === -1 || end <= start) {
throw new Error(`切片失败:${from} → ${to}`);
}
return page.slice(start, end);
};
const list = slice('private function render_list(', 'private function render_add_form(');
const args = slice('private function list_query_args(', 'private function per_page_selector(');
const perpage = slice('private function per_page_selector(', 'private function render_pagenavi(');
const pager = slice('private function render_pagenavi(', 'private function print_assets(');
test('翻页链接使用显式基准 URL,不再依赖 REQUEST_URI', () => {
// 反面写法:只有两个参数的 add_query_arg —— 会退回到 $_SERVER['REQUEST_URI']。
assert.doesNotMatch(page, /add_query_arg\('paged',\s*[^,)]+\)/);
assert.match(pager, /\$base\s*=\s*\$query_args\s*\?\s*add_query_arg\(\$query_args,\s*\$this->page_url\)\s*:\s*\$this->page_url;/);
assert.match(pager, /add_query_arg\('paged',\s*\$page,\s*\$base\)/);
assert.equal(count(pager, /add_query_arg\('paged'/g), 1, '翻页 URL 只应有一个构造点');
});
test('页码夹取到有效范围,禁用状态由夹取后的页码推导', () => {
assert.match(pager, /\$total_pages\s*=\s*max\(1,\s*\(int\)\s*ceil\(\$total_count \/ \$number_per_page\)\)/);
assert.match(pager, /\$current_page\s*=\s*min\(max\(1,\s*\(int\)\s*\(\$_GET\['paged'\] \?\? 1\)\),\s*\$total_pages\)/);
// 越界时 if/elseif 全不命中的分支结构不得回归。
assert.match(pager, /\$on_first\s*=\s*\$current_page <= 1;/);
assert.match(pager, /\$on_last\s*=\s*\$current_page >= \$total_pages;/);
assert.equal(count(pager, /\$on_first \? '#' :/g), 2, '第一页/上一页都要按 on_first 落到 #');
assert.equal(count(pager, /\$on_last \? '#' :/g), 2, '下一页/最后一页都要按 on_last 落到 #');
assert.doesNotMatch(pager, /\$current_page == \$total_pages\b/);
assert.doesNotMatch(pager, /\$prev_page_url\s*=\s*'#';/);
});
test('越界页码回退到最后一页,不再出现无分页条的死胡同', () => {
assert.match(list, /\$max_page\s*=\s*max\(1,\s*\(int\)\s*\(\$result\['max_pages'\] \?\? 1\)\)/);
assert.match(list, /if \(!\$items && \$count_all > 0 && \$paged > \$max_page\)/);
assert.match(list, /\$paged\s*=\s*\$max_page;/);
// 回退之后必须重查,否则表格依旧是空的。
assert.equal(count(list, /\$svc->get_list\(/g), 2);
// 分页条必须在空态分支之后渲染,否则空态 return 会把分页条一起吞掉。
assert.ok(list.indexOf('render_pagenavi(') > list.indexOf('未找到对应内容'), '分页渲染必须晚于空态处理');
});
test('每页条数由界面提供,且与筛选参数保持一致', () => {
assert.match(page, /public const DEFAULT_PER_PAGE = 30;/);
assert.match(list, /\(int\) \(\$_REQUEST\['ice_perpage'\] \?\? self::DEFAULT_PER_PAGE\)/);
assert.match(perpage, /method="get"/);
assert.match(perpage, /name="ice_perpage"/);
assert.match(perpage, /name="page" value="' \. esc_attr\(self::PAGE_SLUG\)/);
assert.match(perpage, /selected\(\$per_page, \$choice, false\)/);
assert.match(perpage, /onchange="this\.form\.submit\(\);"/);
// 非预设值也要出现在下拉里,否则当前状态无法回显、一提交就被重置。
assert.match(perpage, /if \(!in_array\(\$per_page, \$choices, true\)\) \{/);
assert.match(list, /\$query_args = \$this->list_query_args\(/);
assert.match(list, /per_page_selector\(\$per_page, \$query_args\)/);
assert.match(list, /render_pagenavi\(\$count_all, \$per_page, \$query_args\)/);
});
test('筛选参数白名单化拼装,排序与每页条数随链接带走', () => {
for (const key of ['status', 'type', 'other', 's', 'orderby', 'desc', 'ice_perpage']) {
assert.match(args, new RegExp(`\\$args\\['${key}'\\] =`), `缺少 ${key}`);
}
// 默认值不进 URL,避免链接里塞满无意义参数。
assert.match(args, /\$orderby !== 'modified_time'/);
assert.match(args, /\$order === 'ASC'/);
assert.match(args, /\$per_page !== self::DEFAULT_PER_PAGE/);
// paged 由 render_pagenavi 单独追加,不能混进基准参数里(否则会被自己覆盖)。
assert.doesNotMatch(args, /\$args\['paged'\]/);
});
test('禁用的翻页按钮不可点击,避免点空还滚回页首', () => {
assert.match(page, /\.tablenav-pages a\.disabled \{ pointer-events: none; \}/);
});