Files
MailUI4Agents/client/electron/test/harmony-logic.test.mjs
JianFeeeee c6aaf8c468 refactor(harmony): 23 处废弃 API 换成 UIContext 写法(全局 promptAction.showToast 自 API 18 废弃)
SDK 里写得很清楚:`@ohos.promptAction.d.ts` 的全局 `showToast` 标着 `@deprecated since 18`,
替代品是 `UIContext.getPromptAction()`。仓库里有 23 处这种调用(6 个页面,历史遗留)——
这一轮既然在按"用系统方案"整理鸿蒙侧,就一次扫干净,并加判据挡住回潮。

- `promptAction.showToast(...)` → `this.getUIContext().getPromptAction().showToast(...)`(23 处)
- 清掉不再需要的 `promptAction` import(多个 → 只留 `router` 等)
- 新增判据:不得再用全局写法。防的不是这次,而是**新增页面照抄旧代码**这条回退路径 ——
  它编译照样通过、只在真机上行为不同。自检同时验"认得出旧写法"与"不误伤新写法"。
- 顺带把权限徽标那条"点它弹说明"的判据改成钉**非废弃**写法(原来是 `promptAction.showToast(`)。

变异验证:把 `SettingsPage` 的一处改回全局写法 → 判红并点出文件。

验证:`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0(harmony-logic 20 条)。
2026-09-14 14:05:35 +08:00

387 lines
24 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.

/**
* 鸿蒙侧的**可执行判据** —— 跑的是客户端真正会跑的那份逻辑。
*
* 为什么要有这个文件:
*
* 移交信里交代的头号纪律是「判据必须点用户真正会点的那一层」—— WebUI 侧就是因为
* 只验了结构与样式、**一次都没点过**,漏掉了"点日历/联系人不翻页"的 bug 一路到线上。
* 而鸿蒙侧现在**没有设备**`hdc list targets` 为空、模拟器在本机沙箱下起不来),
* 于是"点一下"这件事在鸿蒙上暂时无法自动验。
*
* 应对办法不是编个假判据,而是把**会点的那一层的内核**抽出来:
* `client/harmony/entry/src/main/ets/model/MailGrouping.ts` 是纯逻辑、无 UI 依赖,
* 本文件用 node 的 `--experimental-strip-types` **直接执行它**,断言的是行为
* (折叠后组头是不是最新一封、单封是不是不成组、预算剩 1 个来回是什么档)。
* 页面那一层再用源码判据钉住"确实调了这些函数"—— 两层合起来,
* 「逻辑对」+「页面接上了」都有判据;剩下的"手感/观感"如实标注未验。
*
* 与 WebUI 的对应物:`src/lib/mailGroups.ts`(折叠 / isFlatGroup
* `src/components/WorkCard.tsx` 的 `BudgetChip`(预算档位)、`ContactPanel.tsx`(视图切换)。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } 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');
const MODULE_TS = join(HARMONY_ETS, 'model/MailGrouping.ts');
/** 被测对象:鸿蒙客户端真正引用的那份逻辑(不是复制品) */
const H = await import(pathToFileURL(MODULE_TS).href);
const page = readFileSync(join(HARMONY_ETS, 'pages/MainPage.ets'), 'utf8');
/**
* 断言一律读**剥掉注释的源码**。
*
* 起因是一次变异测试:我把 `promptAction.showToast(` 注释掉,判据**照样绿** ——
* 因为它在注释里也能被正则匹配到。注释里出现某个调用,恰恰说明不了那个调用存在
* (而注释里正当地引用旧写法又是常有的事)。这条纪律在 `cross-client-theme` 里
* 已经用过一次(遮罩那段注释里引用了旧值),这里统一成常态。
*/
const pageCode = page.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
const webGroups = readFileSync(join(ROOT, 'client/electron/src/lib/mailGroups.ts'), 'utf8');
const webCard = readFileSync(join(ROOT, 'client/electron/src/components/WorkCard.tsx'), 'utf8');
/** 造一封邮件(只带折叠/渲染用得到的字段,字段名与 Models.ets 的 MailSummary 一致) */
const mail = (over) => Object.assign(
{
mail_id: 'm-' + Math.random().toString(36).slice(2, 8),
session_id: 's1',
session_alias: '',
from_name: 'pi',
subject: '主题',
body_preview: '预览',
created_at: '2026-09-14T10:00:00Z',
status: 'read',
permission_mode: '',
source_account_id: 'acct-1',
source_account_name: '工作邮箱'
},
over
);
// ───────────────────────── 收件箱折叠 ─────────────────────────
test('折叠:同一个会话的信合成一组,组头取**最新一封**', () => {
const older = mail({ mail_id: 'm-a', session_id: 's1', created_at: '2026-09-14T09:00:00Z', subject: '旧主题', session_alias: '旧别名' });
const newer = mail({ mail_id: 'm-b', session_id: 's1', created_at: '2026-09-14T11:00:00Z', subject: '新主题', session_alias: 'fix-x', status: 'unread' });
const groups = H.groupMailsBySession([older, newer]);
assert.equal(groups.length, 1, '同一个 session_id 应该只有一组');
assert.equal(groups[0].alias, 'fix-x', '组头别名应取最新一封');
assert.equal(groups[0].subject, '新主题', '组头主题应取最新一封(会话主题会随任务推进被改写)');
assert.equal(groups[0].latest.mail_id, 'm-b');
assert.equal(groups[0].unreadCount, 1, '组内未读数');
assert.deepEqual(groups[0].mails.map(m => m.mail_id), ['m-b', 'm-a'], '组内按时间倒序');
});
test('★ 判据自检:把组头当成"第一封"而不是"最新一封"必须判红', () => {
// 故意按"旧 → 新"传入:实现里少了 sort 的话mails[0] 就是旧的,组头会写错
const older = mail({ mail_id: 'm-a', session_id: 's1', created_at: '2026-09-14T09:00:00Z', subject: '旧主题' });
const newer = mail({ mail_id: 'm-b', session_id: 's1', created_at: '2026-09-14T11:00:00Z', subject: '新主题' });
const groups = H.groupMailsBySession([older, newer]);
assert.notEqual(groups[0].subject, '旧主题', '组头取到了旧的那封 —— 折叠没有排序');
});
test('单封的组不算「组」(与 WebUI isFlatGroup 同一结论)', () => {
const one = H.groupMailsBySession([mail({ session_id: 's1' })]);
const two = H.groupMailsBySession([mail({ session_id: 's1' }), mail({ session_id: 's1', created_at: '2026-09-14T11:00:00Z' })]);
assert.equal(H.isFlatGroup(one[0]), true);
assert.equal(H.isFlatGroup(two[0]), false);
// WebUI 侧同一条规则仍在(哪边改了口径,这条会红)
assert.match(webGroups, /export function isFlatGroup\(g: MailGroup\): boolean \{\s*return g\.mails\.length === 1;/, 'WebUI 的 isFlatGroup 口径变了');
});
test('组间按最新一封倒序;时间相同时用 mail_id 倒序兜底', () => {
const a = mail({ mail_id: 'm-1', session_id: 'sa', created_at: '2026-09-14T09:00:00Z' });
const b = mail({ mail_id: 'm-2', session_id: 'sb', created_at: '2026-09-14T12:00:00Z' });
const groups = H.groupMailsBySession([a, b]);
assert.deepEqual(groups.map(g => g.session_id), ['sb', 'sa']);
// 同一时刻mail_id 倒序(与后端 ORDER BY created_at DESC, mail_id DESC 一致)
const t = '2026-09-14T10:00:00Z';
const x = mail({ mail_id: 'm-aaa', session_id: 'sx', created_at: t });
const y = mail({ mail_id: 'm-zzz', session_id: 'sy', created_at: t });
assert.deepEqual(H.groupMailsBySession([x, y]).map(g => g.session_id), ['sy', 'sx']);
});
test('时间解析失败不抛错、也不把顺序交给入参NaN 参与比较恒为 false 的坑)', () => {
const bad = mail({ mail_id: 'm-bad', session_id: 'sbad', created_at: '不是时间' });
const good = mail({ mail_id: 'm-ok', session_id: 'sok', created_at: '2026-09-14T10:00:00Z' });
const forward = H.groupMailsBySession([bad, good]).map(g => g.session_id);
const backward = H.groupMailsBySession([good, bad]).map(g => g.session_id);
assert.deepEqual(forward, backward, '入参顺序换了,结果就变 —— 排序依赖了 NaN 比较');
assert.deepEqual(forward, ['sok', 'sbad']);
});
test('鸿蒙特有:多账号收件箱里,同一 session_id 出现在两个账号是两件事', () => {
const fromA = mail({ mail_id: 'm-a', session_id: 'shared', source_account_id: 'acct-1' });
const fromB = mail({ mail_id: 'm-b', session_id: 'shared', source_account_id: 'acct-2', created_at: '2026-09-14T11:00:00Z' });
const groups = H.groupMailsBySession([fromA, fromB]);
assert.equal(groups.length, 2, '两个账号的同名会话被折叠成了一组 —— 键里少了账号');
});
test('session_id 缺失的信各自成组(一条脏数据不该让整栏空白)', () => {
const a = mail({ mail_id: 'm-1', session_id: '' });
const b = mail({ mail_id: 'm-2', session_id: '' });
const groups = H.groupMailsBySession([a, b]);
assert.equal(groups.length, 2);
assert.equal(H.isFlatGroup(groups[0]), true);
});
// ───────────────────────── 未读数与"这一页可能不全" ─────────────────────────
test('未读数用服务端 total 相加(它是 CountUnread权威负数/0 忽略', () => {
assert.equal(H.sumUnreadTotals([3, 4]), 7);
assert.equal(H.sumUnreadTotals([0, -1, 2]), 2);
assert.equal(H.sumUnreadTotals([]), 0);
assert.match(pageCode, /sumUnreadTotals\(/, '页面要用服务端未读数,而不是数这一页');
});
test('只有"取满了这一页"才提示可能还有更多(服务端 total 是未读数,不是总封数)', () => {
assert.equal(H.partialLoadNotice(12, 50), '', '没取满就别吓人');
assert.equal(H.partialLoadNotice(50, 50), '已加载 50 封(本页上限 50可能还有更多');
assert.equal(H.partialLoadNotice(0, 0), '', 'limit 不合法时不提示');
assert.match(pageCode, /partialLoadNotice\(/, '页面要用这条提示');
});
test('★ 界面不再把"服务端未读数"当成"总封数"显示', () => {
// 原先底部写的是「共 N 封」,而那个 N 是 /me/mail/inbox 的 total= CountUnread
// 于是同一屏上会出现「共 7 封」和「未读 7」这种自相矛盾的两行字。
assert.ok(
!/共 ' \+ this\.total \+ ' 封/.test(pageCode),
'页面里还有「共 N 封」—— 服务端 total 是未读数,不是总封数'
);
assert.match(pageCode, /已加载 ' \+ this\.loaded \+ ' 封/, '应如实说"已加载了多少封"');
});
// ───────────────────────── 预算(与 WebUI BudgetChip 同判据) ─────────────────────────
test('往返预算档位与 WebUI 的 BudgetChip 完全一致', () => {
// WebUI 的判据WorkCard.tsxmax<=0 不显示;剩 0 = 用尽;剩 ≤1 = 将尽;其余普通
assert.match(webCard, /if \(!max \|\| max <= 0\) return null;/, 'WebUI 的"不限不显示"口径变了');
assert.match(webCard, /remaining === 0/, 'WebUI 的"用尽"判据变了');
assert.match(webCard, /remaining <= 1/, 'WebUI 的"将尽"判据变了');
// 鸿蒙侧同一批输入必须给出同样的档位
assert.equal(H.budgetState(0, 0), 'none', '上限 0 = 不限,不显示');
assert.equal(H.budgetState(5, 5), 'spent', '剩 0 = 用尽');
assert.equal(H.budgetState(5, 4), 'warn', '剩 1 = 将尽(快跑满的任务要人介入)');
assert.equal(H.budgetState(5, 3), 'ok');
assert.equal(H.budgetState(5, 9), 'spent', '用超了也是用尽,不能算成还剩负数');
assert.equal(H.budgetLabel(5, 4), '1/5');
assert.equal(H.budgetLabel(0, 0), '', '不限时徽标文字是空串(页面据此不渲染)');
assert.match(pageCode, /budgetLabel\(c\.max_rounds, c\.used_rounds\)/, '卡片上要显示预算');
assert.match(pageCode, /budgetState\(c\.max_rounds, c\.used_rounds\)/, '卡片上要用同一档位判据');
});
// ───────────────────────── 联系人页两种视图为撤掉平级「会话」tab 做准备) ─────────────────────────
test('联系人页的列表/卡片切换:点一次换一次,标题用 WebUI 那套词', () => {
assert.equal(H.nextContactView('list'), 'card');
assert.equal(H.nextContactView('card'), 'list');
assert.equal(H.contactViewTitle('card'), '工作列表', '卡片视图的标题应与 WebUI 一致');
assert.equal(H.contactViewTitle('list'), '联系人');
assert.match(pageCode, /this\.contactView = nextContactView\(this\.contactView\)/, '切换按钮要走这条判据');
assert.match(pageCode, /contactViewTitle\(this\.contactView\)/, '标题要走这条判据');
});
test('卡片上"最新一封是谁发的":人 vs Agent决定人要不要接手', () => {
assert.equal(H.lastFromIsHuman('pi', 'jianf'), true);
assert.equal(H.lastFromIsHuman('pi', 'pi'), false);
assert.match(pageCode, /lastFromIsHuman\(c\.agent_name, c\.last_from\)/, '卡片要用这条判据选图标');
// WebUI 侧同一判据仍在
assert.match(webCard, /const fromHuman = c\.last_from !== c\.agent_name;/, 'WebUI 的 fromHuman 口径变了');
});
// ───────────────────────── 两层的接合:页面确实调了被测逻辑 ─────────────────────────
test('页面把折叠逻辑真正接上了(不是"逻辑写好了没人用"', () => {
assert.match(pageCode, /import \{[\s\S]*groupMailsBySession[\s\S]*\} from '\.\.\/model\/MailGrouping'/, '页面要 import 折叠逻辑');
assert.match(pageCode, /this\.groups = groupMailsBySession\(mergedMails\)/, '加载后要折叠');
assert.match(pageCode, /if \(isFlatGroup\(g\)\)/, '单封的组要平铺渲染(这条就是"点开会多一次点击"的那个分支)');
assert.match(pageCode, /this\.isExpanded\(g\.key\)/, '多封的组要按展开状态渲染');
assert.match(pageCode, /toggleExpanded\(g\.key\)/, '组头要能点开(用户真正会点的那一层)');
assert.match(pageCode, /this\.WorkCard\(c\)/, '卡片视图要真的渲染出来');
});
// ───────────────────────── 撤掉平级「会话」tabP2a 收尾) ─────────────────────────
test('底部只剩 收件箱 / 联系人 两个平级页签,「会话」不再是入口', () => {
/*
* pi 的判断WebUI 里「会话」从来不是一个入口,它是**两处已有视图**(联系人页的卡片视图
* + 收件箱的会话折叠)。鸿蒙原来把它单列成 tab等于把"卡片视图"放错了位置。
* 顺序也按他说的:**先补视图与折叠,再撤 tab** —— 撤早了,
* 往返预算 / status / from_agent 这些只在会话列表里出现的信息就没地方看了。
*/
const tabLabels = [...pageCode.matchAll(/TabBarBuilder\('([^']+)'/g)].map(m => m[1]);
assert.deepEqual(tabLabels, ['收件箱', '联系人'], `平级页签应只剩两个,实际:${tabLabels.join('、')}`);
assert.ok(!/struct\s+SessionsTab/.test(pageCode), 'SessionsTab 已经撤了,不该再留在页面里');
assert.ok(!/sessions\(\)/.test(pageCode), '撤了入口就不该再拉 /me/sessions否则是没人看的请求');
});
test('卡片视图的字段集与 WebUI 的 WorkCard 一致(撤 tab 后"信息没丢"的依据)', () => {
/*
* 撤掉会话列表的前提是"它独有的信息都还在"。这条判据把这个前提变成可执行的:
* 参考实现WebUI `WorkCard.tsx`)显示哪些字段,鸿蒙卡片就得显示哪些 ——
* 少一个说明撤 tab 丢了东西;哪天 WebUI 补了新字段,这条也会红,提醒跟着补。
*/
const fields = src => new Set([...src.matchAll(/\bc\.([a-z_]+)/g)].map(m => m[1]));
const web = fields(webCard);
// 鸿蒙卡片 Builder 的正文(从 `WorkCard(c: Contact)` 到下一个 @Builder 之前)
const cardStart = pageCode.indexOf('WorkCard(c: Contact)');
assert.ok(cardStart > 0, '找不到鸿蒙的卡片 Builder');
const cardBody = pageCode.slice(cardStart, pageCode.indexOf('@Builder', cardStart));
const harmony = fields(cardBody);
const missing = [...web].filter(f => !harmony.has(f));
const extra = [...harmony].filter(f => !web.has(f));
assert.deepEqual(missing, [], `鸿蒙卡片少了 WebUI 卡片有的字段:${missing.join('、')}`);
assert.deepEqual(extra, [], `鸿蒙卡片多了 WebUI 没有的字段(要么补进 WebUI要么说明理由${extra.join('、')}`);
});
// ───────────────────────── 权限档位 + 强制力(与 WebUI PermissionChip 同文案) ─────────────────────────
test('档位标签与强制力标记:和 WebUI 的 MODE_LABEL / ENFORCEMENT_LABEL 逐字一致', async () => {
const webChip = readFileSync(join(ROOT, 'client/electron/src/components/PermissionChip.tsx'), 'utf8');
const labelOf = (name, src) => {
const block = src.slice(src.indexOf(`const ${name}`), src.indexOf('};', src.indexOf(`const ${name}`)));
return new Map([...block.matchAll(/(\w+):\s*'([^']+)'/g)].map(m => [m[1], m[2]]));
};
const webModes = labelOf('MODE_LABEL', webChip);
const webEnf = labelOf('ENFORCEMENT_LABEL', webChip);
for (const [mode, label] of webModes) {
assert.equal(H.permissionLabel(mode), label, `${mode} 档的中文标签两边必须一致`);
}
assert.equal(H.permissionLabel(''), '', '空档位不显示徽标(人→人的信、旧会话)');
for (const [e, label] of webEnf) {
assert.equal(H.enforcementLabel(e), label, `${e} 的强制力标签两边必须一致`);
}
// 认不出的值按 advisory保守方向绝不当成"平台拦得住"
assert.equal(H.enforcementKey('未知'), 'advisory');
assert.equal(H.enforcementKey(''), 'advisory');
assert.match(webChip, /e === 'native' \|\| e === 'partial' \|\| e === 'advisory' \? e : 'advisory'/, 'WebUI 的保守默认值变了');
// 三种强制力必须有**三种**标记WebUI 用实心/靶心/空心三种点,鸿蒙用三个字形)
const glyphs = ['native', 'partial', 'advisory'].map(H.enforcementGlyph);
assert.equal(new Set(glyphs).size, 3, `三种强制力必须形状可辨,实际:${glyphs.join('')}`);
assert.equal(H.permissionChipText('plan', 'partial'), '只读 ◉');
assert.equal(H.permissionChipText('', 'native'), '', '空档位连标记都不该有');
assert.equal(H.enforcementLabel(''), '仅提示', '空/未知强制力必须说"仅提示",不能默认成平台强制');
});
test('★ 档位说明文案与 WebUI permissionModeHint 逐字一致(两边不能给两种保证)', () => {
/*
* 鸿蒙跑不了 TSX文案只能 port 一份;**可执行的比对是它的替代品**
* 从 WebUI 源码里抽出 permissionModeHint 的所有 return 字符串,
* 再要求鸿蒙的 permissionHint 对 3 档 × 3 强制力(外加空/未知)给出的每一句话
* 都在那个集合里 —— 任一边改了口径就红。
*/
const webChip = readFileSync(join(ROOT, 'client/electron/src/components/PermissionChip.tsx'), 'utf8');
const fn = webChip.slice(webChip.indexOf('export function permissionModeHint'));
const body = fn.slice(0, fn.indexOf('\n}'));
const webHints = [...body.matchAll(/return\s+((?:'(?:[^'\\]|\\.)*'\s*\+?\s*)+);/g)].map(m =>
[...m[1].matchAll(/'((?:[^'\\]|\\.)*)'/g)].map(x => x[1]).join('').replace(/\\'/g, "'")
);
assert.ok(webHints.length >= 7, `WebUI 的说明文案应该至少 7 条,实际 ${webHints.length}`);
const modes = ['plan', 'workspace', 'full', ''];
const ens = ['native', 'partial', 'advisory', '未知', ''];
for (const mode of modes) {
const said = new Set();
for (const e of ens) {
const hint = H.permissionHint(mode, e);
assert.ok(hint.length > 0, `${mode}/${e} 必须给出说明`);
assert.ok(webHints.includes(hint), `${mode}/${e} 的文案与 WebUI 不一致:${hint}`);
said.add(hint);
}
// 认不出的强制力要落到保守档(与 advisory 同一句话),不能各自发明措辞
assert.equal(new Set([...said]).size <= 3, true, `${mode} 档的说明不该超过三种措辞`);
}
// three-strikesplan 与 workspace 的三种强制力必须给出三种话WebUI 的硬要求)
for (const mode of ['plan', 'workspace']) {
const three = new Set(ens.slice(0, 3).map(e => H.permissionHint(mode, e)));
assert.equal(three.size, 3, `${mode} 档的三种强制力必须给出三种话`);
}
});
test('徽标真的挂在界面上,且点它能看到那句说明(触屏没有悬停)', () => {
assert.match(pageCode, /permissionChipText\(c\.permission_mode, c\.permission_enforcement\)/, '卡片要用"档位+强制力"的徽标');
assert.match(pageCode, /permissionHint\(c\.permission_mode, c\.permission_enforcement\)/, '点徽标要弹出说明');
/*
* 说明必须挂在这颗徽标**自己的 onClick 里**。
*
* 这条原先只查"页面里出现过 showToast",两次变异都躲过去了:
* ① 把 onClick 体掏空showToast 还在页面别处);
* ② 把 toast 挪到相邻的另一个回调(`onHover`)里。
* 正则窗口分不清"在回调里"和"在回调后面",所以这里做**括号配对**
* 只在那个 onClick 的 `{...}` 里面找。这是"只验结构不算数"的又一个小例子。
*/
const onClickBodyOf = (code, fromIdx) => {
const open = code.indexOf('{', fromIdx);
let depth = 0;
for (let i = open; i < code.length; i++) {
if (code[i] === '{') depth++;
else if (code[i] === '}') {
depth--;
if (depth === 0) return code.slice(open, i + 1);
}
}
return code.slice(open);
};
const chipIdx = pageCode.indexOf('permissionChipText(c.permission_mode');
const clickIdx = pageCode.indexOf('.onClick(', chipIdx);
assert.ok(chipIdx > 0 && clickIdx > chipIdx && clickIdx - chipIdx < 600, '徽标上要有自己的 onClick');
const chipClickBody = onClickBodyOf(pageCode, clickIdx);
// 注意用**非废弃**的写法:全局 `promptAction.showToast` 自 API 18 起废弃
// SDK `@ohos.promptAction.d.ts` 的 `@deprecated since 18`
// 要的是 UIContext 上的那个 —— 这条断言顺带把废弃写法挡在门外。
assert.match(chipClickBody, /\.getPromptAction\(\)\.showToast\(/, '徽标的 onClick 里要弹说明UIContext 的非废弃写法)');
assert.match(chipClickBody, /permissionHint\(/, '弹出来的必须是那句说明');
assert.match(pageCode, /enforcementLabel\(c\.permission_enforcement\)/, '说明里要带强制力标签');
assert.match(pageCode, /permissionLabel\(mail\.permission_mode\)/, '收件箱行要用中文档位');
// 收件箱列表接口没有 enforcement 字段,那里不许凭空画强制力标记
const mailItem = pageCode.slice(pageCode.indexOf('MailItem(mail: MailLike)'));
assert.ok(
!/permissionChipText\(mail\./.test(mailItem),
'收件箱每封邮件里没有 permission_enforcement画强制力标记等于编一个"平台做到了什么"'
);
});
test('★ 不得再用废弃的全局 promptAction.showToastAPI 18 起废弃,要走 UIContext', () => {
/*
* SDK 里写得很清楚:`@ohos.promptAction.d.ts` 的全局 `showToast` 标着
* `@deprecated since 18`,替代品是 `UIContext.getPromptAction()`。
* 本仓库原先有 **23 处**这种调用(不是我写的,是历史)—— 既然这一轮在按
* "用系统方案"整理鸿蒙侧,就顺手一次扫干净,并用判据挡住回潮:
* 新增页面照抄旧写法是最常见的回退路径,而它**编译照样通过**。
*/
const dir = join(ROOT, 'client/harmony/entry/src/main/ets');
const files = [];
const walk = d => {
for (const e of readdirSync(d, { withFileTypes: true })) {
const full = join(d, e.name);
if (e.isDirectory()) walk(full);
else if (/\.(ets|ts)$/.test(e.name)) files.push(full);
}
};
walk(dir);
assert.ok(files.length >= 10, `应扫到至少 10 个源文件,实际 ${files.length}`);
const bad = [];
for (const f of files) {
const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
// `getPromptAction().showToast(` 不算违规:它前面必须有 `get`
for (const m of src.matchAll(/(?<!get)promptAction\.showToast\(/g)) bad.push(f.slice(ROOT.length + 1));
}
assert.deepEqual(bad, [], `这些文件还在用废弃的全局 promptAction.showToast${bad.join('、')}`);
// 反向对照:自检正则要真能认出旧写法、且不误伤新写法
assert.ok(/(?<!get)promptAction\.showToast\(/.test('promptAction.showToast({ message: 1 })'), '自检:认不出旧写法');
assert.ok(!/(?<!get)promptAction\.showToast\(/.test('this.getUIContext().getPromptAction().showToast({})'), '自检:误伤了新写法');
});