Files
lucky_shop/tests/diy-card-mp-compat.test.js
ZF sun 8bdd9f1291 fix(diy-card): 修复两处 WXSS 不兼容 calc() + MP 兼容契约测试
微信小程序 WXSS 不支持 calc(), 两处布局在小程序上会失效:

- mini.vue corner 档: max-width: calc(100% - 170rpx) →
  .mini-head { padding-right: 180rpx }(固定值, 浮层宽 120+偏移 20+呼吸 20)
- list.vue 网格: itemStyle width calc(50% - gap/2) →
  布局盒 50% 宽 + 左右各 half-gap padding, 视觉移入内层 .item-card
  (border/圆角/padding 随之下移, 布局盒透明只算 gutter)
- 新增 tests/diy-card-mp-compat.test.js 20 项: 全组件禁 calc()/var(--)
  (先剥注释再断言) + template 标签配平(栈式匹配, 兼容插槽嵌套 template)
  + 反例自测

注: 渐变角度 linear-gradient(deg,...) 有生产先例(detail/enterprise/
forward 均在生产小程序运行), 不在禁项; 真机渲染需 HBuilderX 构建验证。
验证: 46/46(三个名片测试文件) + npm test 仅剩两个存量失败
2026-10-21 11:58:35 +08:00

97 lines
3.8 KiB
JavaScript

/**
* 名片 DIY 组件 · 微信小程序(WXSS/WXML)兼容契约测试
*
* 背景: 组件在商家端装修页配置, C 端跑在 uniapp 产物(含微信小程序)。
* WXSS 有几个 Web 不支持、小程序也不支持的坑, 本测试把它们固化为禁项:
* 1. calc() —— WXSS 不支持(曾致 corner 档浮层布局/List 网格列宽失效)
* 2. CSS 自定义属性 var(--x) —— 小程序会丢弃内联 style 里的自定义属性
* 3. 模板标签配平 —— 防 <template> 嵌套(插槽)误判/漏闭合导致编译失败
*
* 口径: 禁项断言先剥三类注释(防"注释里写不能用的写法"被算违规);
* 反例: 故意含 calc 的串必须被抓住(证明禁项断言不是恒真)。
*
* 跑法: node --test tests/diy-card-mp-compat.test.js
*/
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const DIY_DIR = path.resolve(__dirname, '../components-diy');
const FILES = ['diy-card-mini.vue', 'diy-card-show.vue', 'diy-card-self.vue', 'diy-card-list.vue', 'diy-card-entry.vue', 'diy-card-common.js'];
const read = f => fs.readFileSync(path.join(DIY_DIR, f), 'utf8');
const strip = s => s
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, '$1');
/** uni-app 编译产物里的 void 元素(无闭合标签) */
const VOID_TAGS = new Set(['image', 'input', 'br', 'hr', 'img']);
/** 标签配平: 先剔除属性串(防 > 藏在引号里误判), 再栈式匹配 */
function checkBalance(tpl) {
const stripped = tpl
.replace(/=\s*"(?:[^"\\]|\\.)*"/g, '=""')
.replace(/=\s*'(?:[^'\\]|\\.)*'/g, "=''");
const stack = [];
const re = /<(\/?)([a-z][a-z0-9-]*)((?:\s[^>]*)?)(\/?)>/g;
let m;
while ((m = re.exec(stripped)) !== null) {
const closing = m[1] === '/';
const tag = m[2];
const selfClose = m[4] === '/';
if (VOID_TAGS.has(tag) || selfClose) continue;
if (!closing) {
stack.push(tag);
} else {
const top = stack.pop();
if (top !== tag) return '期望 </' + top + '> 实为 </' + tag + '>';
}
}
return stack.length ? '未闭合: ' + stack.join(',') : null;
}
for (const f of FILES) {
test('MP禁项: ' + f + ' 代码无 calc()(WXSS 不支持)', () => {
const code = strip(read(f));
assert.ok(!/calc\s*\(/.test(code), f + ' 含 calc(): ' + (code.match(/calc\s*\([^)]*\)/) || [''])[0]);
});
test('MP禁项: ' + f + ' 代码无 var(-- 自定义属性(小程序丢弃)', () => {
const code = strip(read(f));
assert.ok(!/var\(--/.test(code));
});
}
for (const f of FILES.filter(x => x.endsWith('.vue'))) {
test('MP编译: ' + f + ' template 标签配平', () => {
const src = read(f);
const start = src.indexOf('<template>');
const end = src.indexOf('<script');
assert.ok(start >= 0 && end > start, f + ' 缺少 <template>/<script> 结构');
const err = checkBalance(src.slice(start, end));
assert.equal(err, null, f + ' 模板标签问题: ' + err);
});
}
// =============================================================
// 反例(防恒真): 含 calc/未闭合的串必须被抓住
// =============================================================
test('反例: 含 calc 的串被禁项断言抓住', () => {
assert.ok(/calc\s*\(/.test(strip('width: calc(50% - 10rpx);')));
});
test('反例: 未闭合标签被配平检查抓住', () => {
assert.ok(checkBalance('<view><text>x</text>') !== null);
assert.ok(checkBalance('<view><text>x</text></view>') === null);
});
test('反例: 嵌套 template(插槽)不会被误判(配平通过)', () => {
assert.equal(checkBalance('<view><template slot="badge"><text>我的名片</text></template></view>'), null);
});