Files
wordpress-theme-aksrt/tests/widget-system/after-sale-policy-contract.test.cjs
T

438 lines
26 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) 后台 schema(PHP SchemaPayment after_sale 分区)与 SPA JS schema 的键、顺序、
* 类型、默认值 1:1 对齐,且 schema 与侧边页均已注册(/payment/after-sale 可达);
* 2) 运行时(PaymentRuntime)缺键回退 schema 默认、HTML 走 kses、时效边界正确;
* 3) 售后工单:images 列、create 图片清洗与上限、上传路由权限、latest_for_order;
* 4) 订单视图模型输出 after_sale 状态(按钮矩阵的唯一数据源);
* 5) 前台:申请/超时效/售后详细三个独立弹窗、modal-mini + 蓝色炫彩头、placeholder 与
* 图片上传配置化、不可再申请提示原文、订单详情弹窗不再内嵌售后表单(死代码清理)。
*/
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')
const FIELD_ORDER = [
'after_sale_s',
'after_sale_time_limit',
'after_sale_expired_text',
'after_sale_reasons',
'after_sale_desc',
'after_sale_materials',
'after_sale_reason_placeholder',
'after_sale_img_s',
]
/** Extract the balanced [...] block assigned to a PHP array key inside a section body. */
function phpSection(src, sectionKey) {
const start = src.indexOf(`'${sectionKey}' => [`)
assert.notEqual(start, -1, `PHP schema has a ${sectionKey} section`)
const open = src.indexOf('[', start)
let depth = 0
for (let i = open; i < src.length; i++) {
const ch = src[i]
if (ch === "'" || ch === '"') {
const quote = ch
i++
while (i < src.length) {
if (src[i] === '\\') { i += 2; continue }
if (src[i] === quote) break
i++
}
continue
}
if (ch === '[') depth++
else if (ch === ']') {
depth--
if (depth === 0) return src.slice(open, i + 1)
}
}
throw new Error(`unbalanced PHP section: ${sectionKey}`)
}
test('after-sale schema is 1:1 between PHP and SPA', () => {
const php = phpSection(read('src/Admin/Schema/SchemaPayment.php'), 'after_sale')
const js = read('assets/admin/src/schema/sections/payment/afterSale.js')
assert.match(php, /'key'\s*=>\s*'after_sale'/)
assert.match(php, /'title'\s*=>\s*'售后策略'/)
assert.match(php, /'option_key'\s*=>\s*'aksrt_settings'/)
// 字段键、顺序、类型一致(JS 侧 defaults 与 groups 同序,按首次出现去重)
const phpKeys = [...php.matchAll(/'key'\s*=>\s*'([a-z0-9_]+)'/g)].map((m) => m[1])
const jsKeys = [...js.matchAll(/key:\s*'([a-z0-9_]+)'/g)].map((m) => m[1])
const sectionKeys = (keys) => [...new Set(keys.filter((key) => FIELD_ORDER.includes(key)))]
assert.deepEqual(sectionKeys(phpKeys), FIELD_ORDER)
assert.deepEqual(sectionKeys(jsKeys), FIELD_ORDER)
for (const [phpType, jsType] of [
['switch', 'switch'],
['number', 'number'],
['textarea', 'textarea'],
['group', 'group'],
['input', 'input'],
]) {
assert.ok(php.includes(`'type' => '${phpType}'`), `missing PHP field type ${phpType}`)
assert.ok(js.includes(`type: '${jsType}'`), `missing JS field type ${jsType}`)
}
// 默认值一致(开关/时效/占位文案/预置原因/所需材料)
assert.match(php, /'after_sale_s', 'label' => '售后功能', 'type' => 'switch', 'default' => true/)
assert.match(js, /after_sale_s:\s*true/)
assert.match(js, /key: 'after_sale_s', label: '售后功能', type: 'switch', default: true/)
assert.match(php, /'after_sale_time_limit', 'label' => '申请售后时效\(天\)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 3650/)
assert.match(js, /key: 'after_sale_time_limit', label: '申请售后时效\(天\)', type: 'number', default: 7, min: 0, max: 3650/)
assert.match(php, /'after_sale_reason_placeholder', 'label' => '申请说明占位文案', 'type' => 'input', 'default' => '必填,请您详细填写申请原因'/)
assert.match(js, /key: 'after_sale_reason_placeholder', label: '申请说明占位文案', type: 'input', default: '必填,请您详细填写申请原因'/)
assert.match(php, /'after_sale_img_s', 'label' => '申请售后可上传图片', 'type' => 'switch', 'default' => true/)
assert.match(js, /key: 'after_sale_img_s', label: '申请售后可上传图片', type: 'switch', default: true/)
for (const reason of ['质量问题', '假冒品牌', '其他原因']) {
assert.ok(php.includes(`'t' => '${reason}'`), `missing PHP preset reason ${reason}`)
assert.ok(js.includes(`{ t: '${reason}' }`), `missing JS preset reason ${reason}`)
}
for (const material of ['联系邮箱', '联系电话']) {
assert.ok(php.includes(`'t' => '${material}'`), `missing PHP material ${material}`)
assert.ok(js.includes(`{ t: '${material}' }`), `missing JS material ${material}`)
}
// HTML 配置项(超时效提示 / 申请说明)走 kses 通道
assert.match(php, /'after_sale_expired_text', 'label' => '超过时效提示', 'type' => 'textarea', 'sanitize_type' => 'html'/)
assert.match(php, /'after_sale_desc', 'label' => '申请售后说明', 'type' => 'textarea', 'sanitize_type' => 'html'/)
assert.match(js, /key: 'after_sale_expired_text', label: '超过时效提示', type: 'textarea', sanitize_type: 'html'/)
assert.match(js, /key: 'after_sale_desc', label: '申请售后说明', type: 'textarea', sanitize_type: 'html'/)
})
test('after-sale schema and sidebar page are registered under 支付&付费', () => {
const schemaIndex = read('assets/admin/src/schema/sections/payment/index.js')
assert.match(schemaIndex, /import \{ afterSaleSchema \} from '\.\/afterSale\.js'/)
assert.match(schemaIndex, /after_sale:\s*afterSaleSchema/)
const section = read('assets/admin/src/constants/sections/payment.js')
assert.match(section, /key: 'payment-after-sale'/)
assert.match(section, /name: 'PaymentAfterSale'/)
assert.match(section, /label: '售后策略'/)
assert.match(section, /route: '\/payment\/after-sale'/)
assert.match(section, /canonicalDomain: 'payment'/)
assert.match(section, /section: 'after_sale'/)
// 侧栏条目位于提现策略之后(支付&付费末位)
assert.ok(section.indexOf("key: 'payment-after-sale'") > section.indexOf("key: 'payment-withdrawal'"))
const schemaRegistry = read('assets/admin/src/schema/index.js')
assert.match(schemaRegistry, /paymentSchemas/)
})
test('after-sale runtime resolves defaults, overrides and the expiry boundary matrix', () => {
const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'after-sale-policy-harness.php')], {
cwd: root,
encoding: 'utf8',
})
assert.equal(result.status, 0, result.stderr || result.stdout)
const payload = JSON.parse(result.stdout)
assert.deepEqual(payload.defaults, {
enabled: true,
time_limit: 7,
expired_text: '<div>抱歉,您的订单已超过售后申请时效</div>',
reasons: [
{ slug: 'c440cdf71825', name: '质量问题' },
{ slug: '2ead35a456a0', name: '假冒品牌' },
{ slug: 'fba26eab6b2b', name: '其他原因' },
],
materials: ['联系邮箱', '联系电话'],
desc: '',
img_enabled: true,
reason_placeholder: '必填,请您详细填写申请原因',
})
assert.equal(payload.overridden.enabled, false)
assert.equal(payload.overridden.time_limit, 30)
assert.equal(payload.overridden.img_enabled, false)
assert.equal(payload.overridden.reason_placeholder, '请描述问题')
assert.deepEqual(payload.overridden.reasons, [
{ slug: 'c440cdf71825', name: '质量问题' },
{ slug: '327e0ab68567', name: '发错货' },
])
assert.deepEqual(payload.overridden.materials, ['联系邮箱'])
// HTML 配置项剥离 script,保留 div
assert.equal(payload.overridden.expired_text, '<div>已超过时效</div>')
assert.equal(payload.overridden.desc, '<div>请保持商品完好</div>')
assert.deepEqual(payload.expired_ok, {
within: true,
exact_boundary: true,
just_expired: true,
long_expired: true,
no_limit: true,
missing_paid: true,
})
})
test('after-sale tickets persist evidence images and expose the latest row', () => {
const schema = read('src/Core/DBMigratorPart2.php')
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_order_aftersale[\s\S]*?images LONGTEXT DEFAULT NULL[\s\S]*?refund_amount/)
// images 列随 schema 2.5.1 引入:dbDelta 只在版本变化时重跑,列变更必须同步升版本,
// 否则老安装不会 ALTER TABLE,写入直接报 Unknown column 'images'。
const migrator = read('src/Core/DBMigrator.php')
const version = migrator.match(/const SCHEMA_VERSION = '(\d+)\.(\d+)\.(\d+)'/)
assert.ok(version, 'DBMigrator exposes a numeric SCHEMA_VERSION')
const [major, minor, patch] = version.slice(1).map(Number)
assert.ok(
major > 2 || (major === 2 && minor > 5) || (major === 2 && minor === 5 && patch >= 1),
`SCHEMA_VERSION must be at least 2.5.1 after adding order_aftersale.images, got ${major}.${minor}.${patch}`
)
const service = read('src/Payment/AfterSaleService.php')
assert.match(service, /public function create\(int \$user_id, string \$order_number, string \$reason, array \$images = \[\], array \$apply_form = \[\]\)/)
// 结构化申请表单:预置原因 / 申请资料 / 申请说明独立入库(reason 融合串保持原样)
assert.match(service, /\$apply_form = \$this->sanitize_apply_form\(\$apply_form\);/)
assert.match(service, /private function sanitize_apply_form\(array \$form\): array/)
assert.match(service, /'apply_form' => \$apply_form === \[\] \? null : wp_json_encode\(\$apply_form/)
assert.match(service, /\$images = \$this->sanitize_images\(\$images\);/)
assert.match(service, /'images' => wp_json_encode\(\$images\)/)
assert.match(service, /private function sanitize_images\(array \$images\): array \{[\s\S]*?count\(\$clean\) >= 3[\s\S]*?esc_url_raw/)
assert.match(service, /public function latest_for_order\(string \$order_number\): \?array \{[\s\S]*?ORDER BY id DESC LIMIT 1/)
// normalize 输出 images 数组(坏 JSON / 空串回落空数组)
assert.match(service, /json_decode\(\(string\) \(\$row\['images'\] \?\? ''\), true\)/)
// 图片上传路由:登录 + nonce + 售后/图片开关 + 订单归属;回调委托评论图片上传
assert.match(service, /\/orders\/' \. \$number \. '\/after-sale\/upload-image'/)
assert.match(service, /public function require_after_sale_image\(\\WP_REST_Request \$request\)/)
assert.match(service, /get_after_sale_settings\(\)[\s\S]*?empty\(\$settings\['enabled'\]\)[\s\S]*?empty\(\$settings\['img_enabled'\]\)/)
assert.match(service, /get_user_order\(get_current_user_id\(\), \$number\)/)
assert.match(service, /UserController::get_instance\(\)->rest_upload_comment_image\(\$request\)/)
assert.match(service, /'images' => \['required' => false, 'type' => 'array'[\s\S]*?count\(\$value\) <= 3/)
// 提交 REST 透传 images
// rest_submit 同步上报结构化申请表单(reason_label / materials / description)
assert.match(service, /'reason_label' => \(string\) \$request->get_param\('reason_label'\),/)
assert.match(service, /'materials' +\=> is_array\(\$materials\) \? \$materials : \[\],/)
assert.match(service, /'description' +\=> \(string\) \$request->get_param\('description'\),/)
// 既有售后资产契约(锁 / 退款 / 完成)不受影响
assert.match(service, /GET_LOCK\(%s, 5\)/)
assert.match(service, /Order::STATUS_PAID/)
})
test('order view model exposes the after-sale button matrix', () => {
const userOrders = read('src/Payment/UserOrders.php')
assert.match(userOrders, /private function after_sale_state\(string \$group, string \$order_number, string \$paid_at, \?AfterSaleService \$service, array \$records = \[\]\): array \{/)
assert.match(userOrders, /'after_sale' => \$after_sale_state,/)
assert.match(userOrders, /'enabled'\s*=> \$enabled,/)
assert.match(userOrders, /'expired'\s*=> \(bool\) \$expired,/)
assert.match(userOrders, /'has_application' => \$has_application,/)
assert.match(userOrders, /'open'\s*=> \$open,/)
assert.match(userOrders, /'can_apply'\s*=> \$enabled && !\$expired && !\$open,/)
// 可再次申请售后按工单状态推理:rejected(被拒绝)/ cancelled(已取消)允许再次申请;
// pending / processing / approved(流转中)与 completed(已完结)不允许
assert.match(userOrders, /\$reapplyable_statuses = \[AfterSaleService::STATUS_REJECTED, AfterSaleService::STATUS_CANCELLED\];/)
assert.match(userOrders, /'can_reapply'\s*=> \$enabled && !\$expired && \$has_application && in_array\(\$latest_status, \$reapplyable_statuses, true\),/)
assert.match(userOrders, /'latest'\s*=> \$has_application \? \[/)
assert.match(userOrders, /'apply_form'\s*=> is_array\(\$latest\['apply_form'\] \?\? null\) \? \$latest\['apply_form'\] : null,/)
// 仅已支付订单开放;开关关闭整体不处理售后
assert.match(userOrders, /\$enabled = \$group === OrderCenterQuery::STATUS_PAID && !empty\(\$settings\['enabled'\]\);/)
// 时效以支付时间为准
assert.match(userOrders, /\$this->after_sale_state\(\$group, \$order_number, \(string\) \(\$order\['paid_at'\] \?\? ''\), \$after_sale_service, \$after_sales\)/)
// 列表模式单行查询,详情模式复用全量工单
assert.match(userOrders, /\$after_sales = \$detail && \$after_sale_service \? \$after_sale_service->list_for_order\(\$order_number\) : \[\];/)
assert.match(userOrders, /\$latest = \$service->latest_for_order\(\$order_number\);/)
assert.match(userOrders, /\$view\['after_sales'\] = \$after_sales;/)
// 旧 can_after_sale 双路已移除
assert.ok(!userOrders.includes('can_after_sale'), 'can_after_sale must be replaced by the after_sale state')
})
test('frontend after-sale modals are standalone and config-driven', () => {
const js = read('assets/js/order-after-sale.js')
// 四个独立弹窗 + modal-mini 几何 + 蓝色炫彩标题区
assert.match(js, /className: 'aksrt-after-sale-apply-modal'/)
assert.match(js, /className: 'aksrt-after-sale-detail-modal'/)
assert.match(js, /className: 'aksrt-after-sale-expired-modal'/)
assert.match(js, /className: 'aksrt-after-sale-withdraw-modal'/)
assert.match(js, /modal-colorful-header colorful-bg jb-blue/)
assert.match(js, /AksrtModalRuntime\.open\(\{/)
// 滚动容器与禁封申诉弹窗同口径:no-scrollbar 完全隐藏滚动条(含细滚动条),
// 避免 modal-mini 弹窗边缘出现可见滚动栏。
assert.match(js, /'<div class="scroll-y no-scrollbar max-vh5">'/)
assert.match(js, /'<div class="scroll-y no-scrollbar max-vh5 aksrt-after-sale-detail__body">'/)
assert.ok(!js.includes('mini-scrollbar'), 'after-sale modals must not render a visible scrollbar')
// 申请表单:预置原因徽章、所需资料、必填申请说明(placeholder 取后台配置)
assert.match(js, /reasonOptions\(\)/)
assert.match(js, /materialOptions\(\)/)
assert.match(js, /data-aksrt-after-sale-reason=/)
assert.match(js, /data-aksrt-after-sale-material=/)
assert.match(js, /var placeholder = String\(cfg\.reasonPlaceholder \|\| '必填,请您详细填写申请原因'\)/)
assert.match(js, /name="reason"/)
assert.match(js, /notyf\('请填写申请说明', 'danger'\)/)
assert.match(js, /notyf\('请选择售后原因', 'danger'\)/)
// 图片上传:开关控制渲染,最多 3 张,上传/删除/提交
assert.match(js, /if \(cfg\.imgEnabled\) \{/)
assert.match(js, /MAX_IMAGES = 3/)
assert.match(js, /\/after-sale\/upload-image'/)
assert.match(js, /data-aksrt-after-sale-image-remove/)
assert.match(js, /images: collectImages\(shell\)/)
assert.match(js, /reason_label: selectedReasonLabel\(shell\)/)
assert.match(js, /materials: collectMaterials\(shell\)/)
assert.match(js, /description: reasonText,/)
// 申请说明(HTML)+ 超时效提示(HTML)由后台配置直出
assert.match(js, /var desc = String\(cfg\.desc \|\| ''\)/)
assert.match(js, /var text = String\(settings\(\)\.expiredText \|\| '抱歉,您的订单已超过售后申请时效'\)/)
// 售后详细:状态徽章 / 明细行 / 图片缩略图;不可再申请提示原文 + 我知道了
// 申请表单分行展示:原因 / 申请资料 / 申请说明各自独立成行(与申请时间同格式);
// 老数据无 apply_form 时兜底展示融合串
assert.match(js, /detailRow\('原因', String\(applyForm\.reason_label \|\| ''\)\)/)
assert.match(js, /detailRow\('申请说明', String\(applyForm\.description \|\| ''\)\)/)
assert.match(js, /STATUS_LABELS = \{[\s\S]*?pending: '待审核'[\s\S]*?completed: '已完成',/)
// 不可再申请:不再展示阻断文案,仅保留关闭入口;可再申请:仅保留单个主按钮(无「关闭」)
assert.ok(!js.includes('暂不支持再次申请售后'), 'blocked notice copy must be removed')
assert.ok(!js.includes('REAPPLY_BLOCKED_TEXT'), 'blocked text constant must be removed')
assert.ok(!js.includes('>关闭</button>'), 'detail modal must not render a 关闭 button')
assert.match(js, /data-aksrt-modal-close>我知道了</)
assert.match(js, /data-aksrt-after-sale-reapply data-aksrt-after-sale-payload="/)
// 再次申请售后:can_reapply 必然伴随 has_application=true,入口点击须清除标记,
// 否则 openApplyFlow 的 has_application 分流会把用户弹回「售后详细」打转
assert.match(js, /payload\.after_sale\.has_application = false[\s\S]{0,120}openApplyFlow\(payload\)/)
// 撤销售后:入口仅随待审核状态渲染,点击先过二次确认弹窗,确认后 DELETE 工单
assert.match(js, /status === 'pending'[\s\S]*?data-aksrt-after-sale-withdraw="' \+ escapeHtml\(\(order && order\.order_number\) \|\| ''\)/)
assert.match(js, /buildWithdrawHtml\(orderNumber\)/)
assert.match(js, /className: 'aksrt-after-sale-withdraw-modal'/)
assert.match(js, /data-aksrt-after-sale-withdraw-confirm="' \+ escapeHtml\(orderNumber\) \+ '"/)
assert.match(js, /method: 'DELETE'/)
assert.match(js, /AksrtModalRuntime\.close\('withdraw'\)/)
assert.match(js, /notyf\('售后申请已撤销', 'success'\)/)
// 人机验证:提交与撤销前先过验证(场景未启用时挂载点缺失,run 直接放行)
assert.match(js, /function runCaptcha\(scene\)/)
assert.match(js, /runCaptcha\('after_sale_apply'\)/)
assert.match(js, /runCaptcha\('after_sale_cancel'\)/)
assert.match(js, /AKSRTCaptcha\.run\(scene\)/)
// 提交后关闭弹窗并刷新订单列表
assert.match(js, /AKSRTOrderCenter\.refresh\(orderNumber\)/)
assert.match(js, /window\.AksrtModalRuntime\.close\('submit'\)/)
// 入口委托:申请 / 售后详细
assert.match(js, /data-aksrt-after-sale-apply/)
assert.match(js, /data-aksrt-after-sale-detail/)
assert.match(js, /auto_fun\.aksrt-after-sale/)
})
test('order center drops the embedded after-sale form and exposes refresh()', () => {
const js = read('assets/js/order-center.js')
// 内嵌售后表单与两个 submit 处理器已彻底移除(死代码清理)
assert.ok(!js.includes('data-aksrt-after-sale-form'), 'embedded after-sale form must be removed')
assert.ok(!js.includes('提交售后申请'), 'embedded after-sale submit copy must be removed')
assert.ok(!js.includes('onDocumentSubmit'), 'document submit handler for the after-sale form must be removed')
// 列表卡片与订单详情弹窗底部共用新的售后按钮
assert.match(js, /function afterSaleButtons\(order\)/)
assert.match(js, /function afterSalePayload\(order\)/)
assert.match(js, /data-aksrt-after-sale-detail="' \+ payload \+ '"/)
assert.match(js, /data-aksrt-after-sale-apply="' \+ payload \+ '"/)
assert.match(js, /if \(afterSale\.enabled\) html \+= afterSaleButtons\(order\);/)
assert.ok(!js.includes('\n html += afterSaleButtons(order);\n'), 'detail modal footer must not render the shared after-sale buttons')
// 底部按钮与禁封申诉弹窗同式(but-average modal-buts);「售后详细」入口收拢到记录卡片
assert.match(js, /aksrt-order-detail__actions but-average modal-buts/)
assert.match(js, /data-aksrt-after-sale-apply="' \+ afterSalePayload\(order\) \+ '"/)
// 底部「关闭」与售后详细「我知道了」同款(but c-blue,平铺观感由 orders.css 呈现)
assert.match(js, /class="but c-blue" data-aksrt-modal-close>关闭</)
assert.ok(js.includes('查看详细</button>'), 'record card has a view-detail entry')
assert.match(js, /data-aksrt-after-sale-detail="' \+ recordPayload \+ '"/)
// 记录卡片:原因 / 申请理由分行(新数据走 apply_form,老数据兜底融合串)
assert.ok(js.includes('aksrt-order-detail__after-sale-field">原因:'), 'record card shows 原因:')
assert.ok(js.includes('aksrt-order-detail__after-sale-field">申请理由:'), 'record card shows 申请理由:')
assert.match(js, /data-aksrt-after-sale-detail="/)
assert.match(js, /data-aksrt-after-sale-apply="/)
// 订单详情弹窗 CSS:记录卡片「查看详细」foot 右对齐;底部按钮与售后弹窗家族
// 同款平铺观感(modal-runtime 同款规则镜像到吸底 footer),单按钮占满整行。
const ordersCss = read('assets/css/routes/orders.css')
assert.match(ordersCss, /\.aksrt-order-detail__after-sale-foot\s*\{[^}]*justify-content:\s*flex-end/)
assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but\s*\{[^}]*background:\s*transparent/)
assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but\.c-blue\s*\{[^}]*color:\s*var\(--modal-primary/)
assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but:only-child\s*\{[^}]*grid-column:\s*1 \/ -1/)
// 售后记录区保留在订单详情弹窗:记录卡片带申请时间,「查看详细」逐条入口;
// 取消申请按钮与处理器已移除(撤销入口移入售后详细弹窗)。
assert.match(js, /售后记录/)
assert.match(js, /<time>" \+ escapeHtml\(item\.created_at\)/)
assert.ok(!js.includes('data-aksrt-after-sale-cancel'), 'cancel entry moved into the after-sale detail modal')
assert.ok(!js.includes('取消申请'), 'order detail must not render the legacy cancel button')
// refresh 供售后弹窗提交后刷新列表与统计
assert.match(js, /function refresh\(orderNumber\) \{/)
assert.match(js, /window\.AKSRTOrderCenter = \{ init: init, destroy: destroy, refresh: refresh \}/)
assert.match(js, /liveStates\.forEach\(function \(state\) \{/)
})
test('after-sale assets, template buttons and runtime data are wired', () => {
const scripts = read('src/Core/ScriptsPart1b.php')
assert.match(scripts, /'aksrt-order-after-sale'/)
assert.match(scripts, /\/assets\/js\/order-after-sale\.js'/)
const scripts1c = read('src/Core/ScriptsPart1c.php')
assert.match(scripts1c, /'afterSale'\s*=> \(function \(\) \{/)
assert.match(scripts1c, /PaymentRuntime::get_after_sale_settings\(\)/)
assert.match(scripts1c, /'maxImages'\s*=> 3/)
const template = read('inc/helpers/user-center-renderers/profile-orders.php')
assert.match(template, /\$after_sale = is_array\(\$order\['after_sale'\] \?\? null\) \? \$order\['after_sale'\] : \[\];/)
// 载荷随按钮渲染(order-after_sale.js 同步消费),点击即弹窗、无前置请求
assert.match(template, /\$after_sale_payload = wp_json_encode\(\[/)
assert.match(template, /'after_sale'\s*=> \$after_sale,/)
assert.match(template, /data-aksrt-after-sale-detail="<\?php echo esc_attr\(\$after_sale_payload\);/)
assert.match(template, /data-aksrt-after-sale-apply="<\?php echo esc_attr\(\$after_sale_payload\);/)
assert.match(template, /esc_html_e\('售后详细', 'aksrt'\)/)
assert.ok(!template.includes('can_after_sale'), 'template must not use the removed can_after_sale flag')
// 售后人机验证挂载点(申请 / 撤销):场景未启用时 aksrt_render_captcha 无输出
assert.match(template, /function_exists\('aksrt_render_captcha'\)/)
assert.match(template, /aksrt_render_captcha\('after_sale_apply'\)/)
assert.match(template, /aksrt_render_captcha\('after_sale_cancel'\)/)
const css = read('assets/css/frontend/modal-runtime.css')
assert.match(css, /\.aksrt-after-sale-apply-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /\.aksrt-after-sale-detail-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /\.aksrt-after-sale-expired-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /\.aksrt-after-sale-withdraw-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /width: min\(400px, calc\(100vw - 32px\)\)/)
assert.match(css, /\.aksrt-after-sale-apply-modal \.modal-colorful-header/)
assert.match(css, /\.aksrt-after-sale-apply-modal \.colorful-bg > \.colorful-make/)
// 弹窗底部按钮:四个售后弹窗统一全宽平铺贴底,子按钮去独立胶囊观感(覆盖全局按钮网格)
for (const modal of ['apply', 'detail', 'expired', 'withdraw']) {
assert.match(css, new RegExp(`\\.aksrt-after-sale-${modal}-modal \\.but-average\\.modal-buts[\\s\\S]*?margin: 16px -18px -16px`))
assert.match(css, new RegExp(`\\.aksrt-after-sale-${modal}-modal \\.but-average\\.modal-buts > \\.but[\\s\\S]*?border-radius: 0`))
}
assert.match(css, /\.but:only-child \{ grid-column: 1 \/ -1; \}/)
// 撤销售后入口:待审核状态行内的文字式入口按钮
assert.match(css, /\.aksrt-after-sale-detail__statusCell/)
assert.match(css, /\.aksrt-after-sale-withdraw \{[^}]*background: transparent/)
const field = read('assets/admin/src/components/form/AksField.vue')
assert.match(field, /import draggable from 'vuedraggable'/)
assert.match(field, /v-model="repeaterDragList"/)
assert.match(field, /handle="\.aksrt-field__repeater-handle"/)
assert.match(field, /function repeaterDragKey\(item\)/)
})