微信小程序 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 仅剩两个存量失败
97 lines
3.8 KiB
JavaScript
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);
|
|
});
|