Files
wordpress-theme-aksrt/tests/admin/charge-cards-pagination.test.cjs
T
kate522 52d790c654 fix(payment): 卡密管理每页默认 20 条,撤掉工具栏「每页显示」控件
- DEFAULT_PER_PAGE 由 30 改为 20。
- 删除 per_page_selector() 及其表单样式(.aksrt-perpage-form / .order-header-right),
  工具栏恢复为「过滤链接 + 搜索表单」两段式。
  ice_perpage 仍作为 URL 参数被尊重,只是不再提供界面入口。
- 保留上一提交的分页健壮性修复:页码夹取到 [1, total_pages]、翻页链接用显式基准 URL、
  越界页码回退并重查、禁用按钮 pointer-events:none。
- 契约测试同步改写:断言默认值为 20,并显式禁止「每页显示」控件回归。

验证:默认态渲染为「每页 20 共10」,页面内不存在 aksrt-perpage-form;
ice_perpage=5 仍能翻到第 2 页且链接携带该参数;paged=2 越界仍回退到第 1 页的 10 行;
node --test 三套 18/18;php -l 通过;无头 Chrome 截图确认工具栏无多余控件。
2026-09-17 13:03:05 +08:00

115 lines
6.3 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 —— 看上去就是「分页是坏的」。
* 处置:默认值定为 20;**不在工具栏增加「每页显示」控件**(用户明确要求),
* ice_perpage 仅作为 URL 参数被尊重。
*
* 本契约固化四件事:
* - 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导;
* - 翻页链接基于显式基准 URL(page_url + 当前筛选参数),不再依赖 REQUEST_URI;
* - 越界页码回退到最后一页并重查,不留空态死胡同;
* - 每页条数默认 20,且工具栏不得新增「每页显示」控件。
*/
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 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('每页条数默认 20,工具栏不新增「每页显示」控件', () => {
assert.match(page, /public const DEFAULT_PER_PAGE = 20;/);
assert.match(list, /\(int\) \(\$_REQUEST\['ice_perpage'\] \?\? self::DEFAULT_PER_PAGE\)/);
// 明确约定:不在列表工具栏增加「每页显示」下拉/按钮。任何一处回来都视为回退。
assert.doesNotMatch(page, /per_page_selector/);
assert.doesNotMatch(page, /PER_PAGE_CHOICES/);
assert.doesNotMatch(page, /aksrt-perpage-form/);
assert.doesNotMatch(page, /name="ice_perpage"/);
assert.doesNotMatch(page, /order-header-right/);
// 但 ice_perpage 仍作为 URL 参数被尊重(无界面入口,供 URL / 程序化调用)。
assert.match(args, /\$args\['ice_perpage'\] =/);
assert.match(list, /\$query_args = \$this->list_query_args\(/);
assert.match(list, /render_pagenavi\(\$count_all, \$per_page, \$query_args\)/);
// 工具栏恢复成「过滤链接 + 搜索表单」两段式。
assert.match(list, /echo '<div class="order-header"><ul class="subsubsub">' \. \$but_html \. '<\/ul>' \. \$search \. '<\/div>';/);
});
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; \}/);
});