★ 这一轮从用户一句「滑动手势呢?」开始。查下去发现它不是"顺手加个手势",
而是 `docs/DEBTS.json` 里挂着的一笔债 —— `gesture-semantics` 的原话是:
「P6 第 3 步:鸿蒙侧出现滑动手势代码时**立即建**判据
(此前建 = 只有一端存在的假判据)」
也就是**先有手势、再钉语义**。WebUI 2026-09-14 就有滑动翻页(用户当时
亲口提的),鸿蒙一直没有 ⇒ 之前建判据会是空真(∀x∈∅)。
## 一、手势本体(两端语义逐项对齐,数值各自定)
按 `HARMONY-ALIGN-PLAN.md:118-128` 显式选的 **(b) 口径**:
「手势的物理量本来就不该强求同值……该对齐的是**语义层**」。
· 判定逻辑放**纯逻辑层** `model/Calendar.ts` 的 `judgeSwipe`(可被 node 直跑,
写在 .ets 里就跑不了判据,语义没法被单测钉住)。四道门:位移 / 纵向优先 /
快滑窗口 / 方向。
· 阈值**不引用** WebUI 的 40 / 1.5 / 600(那是把巧合当契约),
各自定为 56vp / 1.4× / 700ms,并在注释里写出取值依据。
· 接线在 `CalendarPage.ets`:`PanGesture({direction: Horizontal})` +
`onActionStart`(记时 —— `GestureEvent` **没有时间戳字段**,我查了 SDK
的 gesture.d.ts 确认)+ `onActionEnd`(读 offsetX/offsetY)。
· ★ 挂在**网格列**上而不是整页:右栏(日程/编辑器)里有输入框与可滚内容,
整页挂会让「在表单里横划一下」变成翻月。
· ★ 翻页复用 `shiftRange()`(与 ‹ › 按钮**同一个来源**)—— WebUI 的注释
专门交代过:各写一套的话,阈值、边界、三档行为迟早分叉。
手势回调里**不准**直接改 year/month(锚点是唯一真相,年月只能由
`shiftRange → syncYearMonthFrom` 派生)。
**有意差异(记录在案,不是漏做)**:WebUI 在周/日档会额外检查「触点是否落在
可横向滚动的区域里」,是则让给滚动条(用户 2026-09-14 报过这个冲突)。
鸿蒙周档是「一行 7 格按 layoutWeight 等分」、**不横滚** ⇒ 该条件不适用。
哪天加了横滚必须同时补上它。
## 二、判据(8 条语义契约 + 行为层)
新增 `cross-client-gesture.test.mjs`:两端都真有手势 / 方向映射逐项相同 /
纵向优先 / 快滑窗口 / 复用同一翻页函数 / 无边界回弹 / 有意差异被记录 / 自检。
**只比语义、不比数值**,并反向断言鸿蒙的阈值常量不得直接取 WebUI 的那三个数。
`harmony-logic.test.mjs` 加行为判据:真跑 `judgeSwipe`,把四道门各自验一遍
(只钉字符串的话,一个 return 写漏了照样全绿)。
## 三、顺手修掉的三处**判据基建**缺陷(不修就没法验证上面这些)
1. `run-all.mjs` 只认 `# pass N`,而 node v24 打的是 `ℹ pass N`
⇒ **21 个文件被记成"没自报条数"**、套件在 HEAD 就恒红(memory 里记过这条,
修法也记过,今天终于落进代码:4 个正则加 `(?:#|ℹ)`)。修完 `unreported` 27 → 0。
代价是暴露出一批此前被"没自报"掩盖的真实问题(下面 4~6 条)。
2. `harmony-nav` 的设备判据 `navItemsOf`:宽屏过滤条件从「左边缘靠左 1/6」
改成「**整个盒子在侧栏轨道内**」。旧条件把**日历网格的格子**
(实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
这是设备实测抓出来的 —— 我第一版还以为是"底部簇混进来了",
打印真实数据才发现是隔壁页面的格子。
3. `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS` **各自的数组**里取
label。原先把全文件 `label:` 一网打尽 ⇒ 得到 7 个(4+3 混在一起),
任何一边改对了它都会红。
## 四、被判据拦住后的正经修法(每条都按判据自己给的方向改,不改判据迁就代码)
· `cross-client-theme` A2 拦住我:新增的 `navActiveBg`/`navBrandFg`/`badgePlain`
未登记;又拦住我:`sseColorOf` 里四个裸色值。→ 抽成 `Theme.sseConnected` 等
四个令牌(取值对齐 WebUI 的 Tailwind 类)+ 登记 + 在 Theme.ets 的表里写理由。
· 同一条判据的"死令牌"检出:`Theme.durBase` 声明了却从没人读。
**查 WebUI 才发现壁纸淡入是真有的动效**(`index.css:742-752` 的
`.app-backdrop` 从 opacity:0 → 1,180ms)⇒ 补上而不是删令牌
(删掉等于把差异抹平、还说成"清理")。reset 在 `animateTo` **外**,
与 `calPaneIn` 同一条纪律。
· 玻璃登记:`NavItemBuilder` → `SidebarItem`(重构后按最近的 @Builder 命名),
登记同步跟上。
· `harmony-admin` 的退出判据:退出逻辑抽成 `api/Logout.ets` 的 `performLogout()`
(两个入口——「我的」页与侧栏底簇——必须做同一件事,尤其"先注销推送 token"
那一步)。判据相应改成**追到实际执行处**(两半都断:按钮调了 + 函数真清了全部),
并写明"别再退回直接匹配 SETTINGS_PAGE 的写法"(那会随重构假红,
下一个人只会去改判据而不看行为)。
· `harmony-widescreen` 的连接点色值:色值搬进 Theme 后,判据改成断
「令牌定义对了 + 侧栏真的用了它」两半(只断任一半都有假绿形态)。
· `align-refs`:`CalendarView.tsx` 变了,按判据要求**读一遍差异**再更新登记
(差异只有农历小字的灰阶档位 gray-300→gray-400/500,**骨架未变**)。
· `criteria-hygiene`:`harmony-contacts` 自造了一个 `code()`、`harmony-widescreen`
裸用 `readFileSync` ⇒ 都改用 `lib/read.mjs` 的共享入口。
途中撞出一个**判据自己的 bug**:`code()` 的块注释正则
`/\*[\s\S]*?\*\//` 会把注释里出现的 `/*`(如 `/」**` 这种中文夹星号)
当成块注释起点,一路吃到几十行后的 `*/`,把中间的 import 全吞掉 ——
于是 hygiene 判据假红"没 import"。改掉那处写法后正常。
## 五、验证
判据面:`run-all.mjs` → `files=32 ran=32 checks=497 pass=497 fail=0
skip=0 red=0 broken=0 unreported=0`。
其中新/改判据:gesture 8、nav 18、widescreen 7、logic 30、admin 27、
cross-client-theme 15、criteria-hygiene 6。
构建:`hvigorw assembleHap` 成功;前端 `npm run build` + 重新打 AppImage/deb
(`build-stamp` 7/7、`packaging` 5/5)。
**设备实测(HATriple 三折叠 3184×2232,hdc 连 127.0.0.1:5555)**:
· 农历在格子里真的显示(1=二十 / 7=廿六 / 19=**初九** / 11=八月),与 WebUI 一致;
这条同时验证了**服务端农历路由已部署**(之前线上是 404)。
· 日历左右两栏:编辑器出现在**右栏**、左栏月份仍可见(单栏模式下编辑器会整页盖掉它)。
· 侧栏 3 项 + 底部一簇;徽标回到图标右上角。
**仍未验(如实标注)**:滑动翻页的**手感**(阈值 56vp/1.4×/700ms 是否合适)
只能真人滑过才知道;我只验了判定逻辑与接线形态。动画同理 ——
机制已验证(`animateTo` 驱动 + reset 在窗口外),但"看起来顺不顺"未做取样验证。
docs:`DEBTS.json` 销掉 `gesture-semantics`(并记结算说明)、
`HARMONY-ALIGN-PLAN.md` P6 从「✅(滑动翻页除外)」改为 ✅。
240 lines
13 KiB
JavaScript
240 lines
13 KiB
JavaScript
/**
|
||
* 左右滑动翻页的**语义契约**:两端逐项相同,数值各自定。
|
||
*
|
||
* 这条判据是 `docs/DEBTS.json` 的 `gesture-semantics` 那条债的还款 ——
|
||
* 它的原文是:
|
||
*
|
||
* 「P6 第 3 步:鸿蒙侧出现滑动手势代码时立即建
|
||
* (此前建 = 只有一端存在的假判据)」
|
||
*
|
||
* 也就是**先有手势、再钉语义**。2026-09-19 鸿蒙侧真的加了手势
|
||
* (`CalendarPage.ets` 的 `PanGesture` + `model/Calendar.ts` 的 `judgeSwipe`),
|
||
* 所以现在建。
|
||
*
|
||
* ★ 为什么钉语义而不是钉数值
|
||
*
|
||
* `docs/HARMONY-ALIGN-PLAN.md:118-128` 显式选了 (b) 口径,理由是原话:
|
||
* 「手势的物理量本来就不该强求同值。40px 的位移阈值、600ms 的"快滑"判据,
|
||
* 在触摸屏与鼠标上、在手机与大屏上,人体工学的合理值天然不同;
|
||
* 强行同值会得到一个两边都不舒服的数。该对齐的是**语义层**。」
|
||
*
|
||
* 所以本文件**从两端源码里各自读一遍语义**(不是读常量名、不是读注释),
|
||
* 再逐项比对:
|
||
*
|
||
* | 语义 | WebUI 侧证据 | 鸿蒙侧证据 |
|
||
* |------------|-------------------------------------------|-------------------------|
|
||
* | 左滑=下一段 | `shift(dx < 0 ? 1 : -1)` | `dx < 0 ? 1 : -1` |
|
||
* | 右滑=上一段 | 同上 | 同上 |
|
||
* | 纵向优先 | `Math.abs(dx) < Math.abs(dy) * 1.5` | `Math.abs(dx) < Math.abs(dy) * X` |
|
||
* | 慢拖不翻页 | `Date.now() - start.t < 600` | `ms > Y` 时 return |
|
||
* | 无边界回弹 | `addMonths` 无钳制(可任意前后) | 同(`addMonths`) |
|
||
* | 手势复用按钮逻辑 | 手势与按钮都调 `shift()` | 手势与按钮都调 `shiftRange()` |
|
||
*
|
||
* 数值(`40` / `1.5` / `600`)**只做形状校验**(是数字、是正数),
|
||
* 断言它们**相等**才是错的 —— 那会把 (b) 口径推翻。
|
||
*/
|
||
import { code, prose } from './lib/read.mjs';
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = join(HERE, '..', '..', '..');
|
||
const HARMONY_ETS = join(ROOT, 'client/harmony/entry/src/main/ets');
|
||
|
||
/** WebUI 的手势实现在日历组件里 */
|
||
const webCal = code(join(ROOT, 'client/electron/src/components/CalendarView.tsx'));
|
||
/** 鸿蒙的判定逻辑在纯逻辑层(可被 node 直接执行,另有行为判据跑它) */
|
||
const harmonyLogic = code(join(HARMONY_ETS, 'model/Calendar.ts'));
|
||
/** 鸿蒙的接线(手势挂在哪个容器上) */
|
||
const harmonyPage = code(join(HARMONY_ETS, 'pages/CalendarPage.ets'));
|
||
|
||
/* ────────────────────────── ① 两端都真的存在手势 ────────────────────────── */
|
||
|
||
test('两端都有左右滑动翻页的代码(不是只有一端)', () => {
|
||
/*
|
||
* 这是本文件存在的前提。`docs/HARMONY-ALIGN-PLAN.md:136-140` 记过:
|
||
* 鸿蒙侧当时**没有日历页、也没有任何手势代码**,此时"两端逐项相同"
|
||
* 只有一端存在 ⇒ 任何断言都是空真(`∀x∈∅`)。所以第一条就把这个前提钉住。
|
||
*/
|
||
assert.match(webCal, /onTouchStart/,
|
||
'WebUI 侧要有触摸起点(`CalendarView.tsx` 的 onTouchStart)');
|
||
assert.match(webCal, /onTouchEnd/,
|
||
'WebUI 侧要有触摸终点(判定位移与时长的地方)');
|
||
|
||
assert.match(harmonyPage, /PanGesture\s*\(/,
|
||
'鸿蒙侧要有手势(`PanGesture`)—— 没有它本判据是空真,正是那条债警告的事');
|
||
assert.match(harmonyLogic, /export function judgeSwipe\s*\(/,
|
||
'鸿蒙侧的判定要落在**纯逻辑层**(`model/Calendar.ts` 的 judgeSwipe)—— ' +
|
||
'写在 .ets 里就跑不了 node 判据,语义没法被单测钉住');
|
||
});
|
||
|
||
/* ────────────────────── ② 语义逐项相同(不比数值) ────────────────────── */
|
||
|
||
test('★ 语义:左滑 = 下一段,右滑 = 上一段(两端同)', () => {
|
||
/*
|
||
* 这是最容易写反、也最容易被"看起来在动"掩盖的一条:
|
||
* 方向反了功能完全正常,只是与另一端的体感相反 —— 用户跨端用会觉得很怪。
|
||
*
|
||
* 两端的写法都必须是 `dx < 0 ? +1 : -1` 这个**同一个形状**:
|
||
* 左滑(dx 为负)= 前进(+1);右滑(dx 为正)= 后退(-1)。
|
||
*/
|
||
const webDir = webCal.match(/shift\(dx\s*<\s*0\s*\?\s*(\d+)\s*:\s*(-?\d+)\)/);
|
||
assert.ok(webDir,
|
||
'WebUI 的方向映射要能读出来(形如 `shift(dx < 0 ? 1 : -1)`)');
|
||
assert.equal(webDir[1], '1', 'WebUI:左滑(dx<0)应映射为 +1(下一段)');
|
||
assert.equal(webDir[2], '-1', 'WebUI:右滑应映射为 -1(上一段)');
|
||
|
||
const hDir = harmonyLogic.match(/delta\s*=\s*dx\s*<\s*0\s*\?\s*(\d+)\s*:\s*(-?\d+)/);
|
||
assert.ok(hDir,
|
||
'鸿蒙的方向映射要能读出来(形如 `v.delta = dx < 0 ? 1 : -1`)');
|
||
assert.equal(hDir[1], webDir[1],
|
||
'★ 左滑在两端的映射必须相同(这里**该**比数值,因为它是语义不是物理量)');
|
||
assert.equal(hDir[2], webDir[2],
|
||
'★ 右滑同理');
|
||
});
|
||
|
||
test('★ 语义:纵向优先 —— 横向位移必须明显大于纵向,两端都有这道门', () => {
|
||
/*
|
||
* 移动端最容易犯的手势错误:用户在纵向列表上滚,手指天然带一点横向偏移,
|
||
* 不卡这条就会"滚着滚着翻页了"。
|
||
*
|
||
* 两端都要有这道门,且形状必须是「横向 < 纵向 * 倍数 ⇒ 放弃」。
|
||
* 倍数是多少**不比**((b) 口径),但必须是个正数。
|
||
*/
|
||
const webAxis = webCal.match(/Math\.abs\(dx\)\s*<\s*Math\.abs\(dy\)\s*\*\s*([\d.]+)/);
|
||
assert.ok(webAxis, 'WebUI 要有"横向 vs 纵向"这道门');
|
||
assert.ok(Number(webAxis[1]) > 1,
|
||
`WebUI 的倍数应 > 1(实测 ${webAxis[1]})—— 等于 1 时斜滑就翻页,判据等于没有`);
|
||
|
||
const hAxis = harmonyLogic.match(/Math\.abs\(dx\)\s*<\s*Math\.abs\(dy\)\s*\*\s*([A-Z_]+|[\d.]+)/);
|
||
assert.ok(hAxis, '鸿蒙要有"横向 vs 纵向"这道门(漏了它 = 纵向滚动会误翻页)');
|
||
|
||
/* 数值不同是**允许的**((b)),但取值来源要说清是常量而不是裸字面量 */
|
||
assert.match(harmonyLogic, /export const SWIPE_AXIS_RATIO:\s*number\s*=\s*[\d.]+/,
|
||
'鸿蒙的倍数要走具名常量(裸字面量会让"这个数为什么是它"无从追溯)');
|
||
});
|
||
|
||
test('★ 语义:慢拖不翻页("快滑"是感知档,不是同一个毫秒数)', () => {
|
||
/*
|
||
* WebUI:`const fast = Date.now() - start.t < 600` + `!fast → return`。
|
||
* 鸿蒙:`if (ms > SWIPE_MAX_DURATION_MS) return v;`。
|
||
*
|
||
* 两端都要有这道门;**毫秒数不比**。
|
||
*/
|
||
const webFast = webCal.match(/Date\.now\(\)\s*-\s*start\.t\s*<\s*(\d+)/);
|
||
assert.ok(webFast, 'WebUI 要有"快滑"时间窗(否则慢慢拖也会翻页)');
|
||
assert.ok(Number(webFast[1]) > 0, 'WebUI 的时间窗应为正数');
|
||
|
||
const hFast = harmonyLogic.match(/ms\s*>\s*SWIPE_MAX_DURATION_MS/);
|
||
assert.ok(hFast, '鸿蒙要有"快滑"时间窗这道门');
|
||
assert.match(harmonyLogic, /export const SWIPE_MAX_DURATION_MS:\s*number\s*=\s*\d+/,
|
||
'鸿蒙的时间窗要走具名常量');
|
||
|
||
/*
|
||
* ★ 反向断言:鸿蒙**不得**引用 WebUI 的那三个数。
|
||
* 这正是 (b) 口径的落点 —— 一旦有人"抄过去省事",两端就被锁在一起,
|
||
* 以后为大屏调阈值会同时改坏手机端。计划文档明写「鸿蒙**不得引用** WebUI 的三个数」。
|
||
*/
|
||
for (const forbidden of ['40', '600', '1.5']) {
|
||
const asStandalone = new RegExp(`SWIPE_[A-Z_]*:\\s*number\\s*=\\s*${forbidden.replace('.', '\\.')}\\b`);
|
||
assert.ok(!asStandalone.test(harmonyLogic),
|
||
`★ 鸿蒙的阈值常量不得直接取 WebUI 的值 ${forbidden} —— ` +
|
||
'(b) 口径要求数值各自定(见 HARMONY-ALIGN-PLAN.md:118-128)');
|
||
}
|
||
});
|
||
|
||
test('★ 语义:手势与翻页按钮复用**同一个**翻页函数(两端同)', () => {
|
||
/*
|
||
* WebUI 的原话:「复用 shift() —— 手势与「上一月/下一月」按钮必须是同一套翻页逻辑,
|
||
* 各写一遍的话阈值、边界、"周/日/月"三种刻度的行为迟早分叉。」
|
||
*
|
||
* 鸿蒙对应的函数是 `shiftRange()`(`shiftMonth` 只是它的旧名包装)。
|
||
* 断言方式:页面里出现的手势判定结果必须交给 shiftRange,
|
||
* 而不是自己算日期/自己改 year/month。
|
||
*/
|
||
assert.match(harmonyPage, /if\s*\(v\.turned\)\s*\{[\s\S]{0,60}this\.shiftRange\(v\.delta\)/,
|
||
'★ 手势判定通过后必须调 `shiftRange(v.delta)` —— 不能自己改 year/month,' +
|
||
'那会出现"手势翻 1 天、按钮翻 7 天"的分叉');
|
||
|
||
/* 手势那一支里不得直接写 year/month(那是 shiftRange 的职责) */
|
||
const gestureBlocks = harmonyPage.match(/\.onActionEnd\([\s\S]{0,400}?\}\)/g) ?? [];
|
||
assert.ok(gestureBlocks.length >= 1, '要能找到手势的 onActionEnd 回调');
|
||
for (const b of gestureBlocks) {
|
||
assert.ok(!/this\.(year|month)\s*=/.test(b),
|
||
'★ 手势回调里不得直接改 `year`/`month` —— 锚点是唯一真相,' +
|
||
'年月只能由 `shiftRange` → `syncYearMonthFrom` 派生');
|
||
}
|
||
});
|
||
|
||
test('★ 语义:翻页无边界回弹(两端一致)', () => {
|
||
/*
|
||
* 语义表里那一行是「翻页边界是否回弹」。两端的答案都是"没有边界":
|
||
* `addMonths(year, month, delta)` 直接把 month 折进 year(`total % 12`),
|
||
* 不钳制范围 ⇒ 可以一直往前/往后翻,不存在"到头了"的状态,
|
||
* 因此**没有回弹动画可看**(不是"忘了做回弹")。
|
||
*
|
||
* 这条判据防的是"哪天有人加了钳制":加了钳制就得同时定义回弹语义,
|
||
* 而那是一个两端都要改的契约变更,不能只改一边。
|
||
*/
|
||
const webAddMonths = webCal.includes('addMonths');
|
||
assert.ok(webAddMonths, 'WebUI 翻页走 addMonths(无钳制)');
|
||
/*
|
||
* 锚点要短:`addMonths` 只有两行,中间隔不了多少字符。
|
||
* (第一版写 `[\s\S]{0,140}?total\s*%\s*12` 反而没匹上 —— 我把它想长了。)
|
||
*/
|
||
assert.match(harmonyLogic, /export function addMonths\([\s\S]*?total % 12/,
|
||
'★ 鸿蒙的 addMonths 必须仍是取模折年(有边界就不对称了);' +
|
||
'若哪天要加钳制,回弹语义必须两端一起定');
|
||
});
|
||
|
||
/* ─────────────────────── ③ 有意差异被记录(不是漏做) ─────────────────────── */
|
||
|
||
test('有意差异:周档的"横滚冲突让位"在鸿蒙侧不存在 —— 但必须被记录', () => {
|
||
/*
|
||
* WebUI 在**周/日档**额外检查「触点是否落在可横向滚动的区域里」,
|
||
* 是则把手势让给滚动条。用户 2026-09-14 亲口报过这个冲突:
|
||
* 「横向滚动条会与切换视图的手势冲突,我觉得周视图需要卡严条件」。
|
||
*
|
||
* 鸿蒙侧**没有这个冲突**:周档是"一行 7 格、按 layoutWeight 等分",不横滚。
|
||
* 所以这一条**不适用**,不是漏做。
|
||
*
|
||
* 判据两半(缺一不可):
|
||
* ① 鸿蒙的周档确实不横滚(否则就该补卡严条件,而它没补 = 真漏);
|
||
* ② 这件事在代码里被**记录下来**(下一个人看到 WebUI 有、鸿蒙没有时,
|
||
* 要能查到这是有意的,而不是当成待办)。
|
||
*/
|
||
const strictWeb = /startsInHorizontalScroller/.test(webCal);
|
||
assert.ok(strictWeb, 'WebUI 侧应有"落在横滚区域就让位"的检查');
|
||
|
||
assert.ok(!/Scroll\s*\(\s*\)\s*\{[\s\S]{0,200}?calScale\s*===\s*'week'/.test(harmonyPage),
|
||
'鸿蒙周档目前不横滚(若改成横滚,必须同时补上这条卡严条件)');
|
||
|
||
/*
|
||
* ★ 读**注释**要用 `prose`(不剥注释),不能用 `code`:
|
||
* 这条断言判的正是「理由有没有被写下来」,而理由天然在注释里。
|
||
* 我第一版用了 code(),它把注释剥掉了 ⇒ 永远红。
|
||
*/
|
||
const harmonyLogicProse = prose(join(HARMONY_ETS, 'model/Calendar.ts'));
|
||
assert.match(harmonyLogicProse, /有意\*\*未对齐\*\*的一条|有意未对齐/,
|
||
'★ 鸿蒙侧要把"这条为什么不适用"写进代码注释 —— ' +
|
||
'两端行为不同的地方不留记录,下一个人只会当成漏做(§7.12 的同一纪律)');
|
||
});
|
||
|
||
/* ─────────────────────────── ④ 判据自检 ─────────────────────────── */
|
||
|
||
test('★ 判据自检:方向写反必须判红', () => {
|
||
/*
|
||
* 没有自检的判据是"看起来在跑"的判据。这里模拟"有人把左滑写成上一段",
|
||
* 确认 ② 那条能红 —— 若不能红,说明它锚错了地方(读的是别的字符串)。
|
||
*/
|
||
const mutated = harmonyLogic.replace(/delta\s*=\s*dx\s*<\s*0\s*\?\s*1\s*:\s*-1/,
|
||
'delta = dx < 0 ? -1 : 1');
|
||
assert.notEqual(mutated, harmonyLogic, '变异要有实际效果(锚点必须命中)');
|
||
/* 变异后**不能**再匹配上「左滑=+1」那个形状 —— 这才是那条断言真正的咬法 */
|
||
const stillOk = /delta\s*=\s*dx\s*<\s*0\s*\?\s*1\s*:\s*-1/.test(mutated);
|
||
assert.equal(stillOk, false,
|
||
'★ 方向写反时,②「左滑=下一段」那条必须能判红');
|
||
});
|