feat(ds-flow): Phase 4.2/4.3/4.5 — CJK golden 字体 + timer 泄漏修复 + 基线重生成

4.5 harness/资源修复:UpdateNotifier 初始延迟由不可取消的 Future.delayed 改为
可取消 Timer,onDispose 一并取消——修 golden 整屏挂载时 pending-timer 泄漏
(生产也受益,provider dispose 不再悬挂 timer)。

4.2 CJK 测试字体:拷生产子集 NotoSansSC-Regular-subset.otf 进 test/fonts/ +
flutter_test_config 注册(family 'Noto Sans SC'),golden 中文真渲染不出豆腐块。

顺带修 tablet_pages_golden_test 既存编译错(6 语言改动遗留):localeProvider
override 返回值 AppLang → 改返回 LocaleNotifier 子类 _FixedLocale(钉死语言态)。

4.3 权威 Linux 容器(ghcr.io/cirruslabs/flutter)重生成全量 golden 基线:
34 tests All passed,32 张 PNG 更新(CJK 真字 + 暖阴影 + tablet zh/en 矩阵)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-08 01:27:09 +08:00
parent bb5acb6c1e
commit c4eb92fc2d
39 changed files with 72 additions and 57 deletions
+11 -2
View File
@@ -47,11 +47,20 @@ class AppUpdateInfo {
/// 更新检查 Notifier。build() 惰性触发:延迟首查 + 每小时轮询。
class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
Timer? _timer;
Timer? _initialTimer;
@override
Future<AppUpdateInfo?> build() async {
ref.onDispose(() => _timer?.cancel());
await Future<void>.delayed(_kInitialDelay);
// 两个 timer 都在 onDispose 取消。初始延迟用可取消的 Timer(而非
// Future.delayed:其内部 timer 无法取消,provider 在延迟期间被 dispose
// 时会悬挂 → widget 测试报 pending timer、生产留资源)。
ref.onDispose(() {
_initialTimer?.cancel();
_timer?.cancel();
});
final ready = Completer<void>();
_initialTimer = Timer(_kInitialDelay, ready.complete);
await ready.future; // 若延迟期间被 dispose,_initialTimer 取消 → 永不 complete,build 中止
final first = await _check();
_timer = Timer.periodic(_kPollInterval, (_) async {
state = AsyncValue.data(await _check());
+3
View File
@@ -20,6 +20,9 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) async {
'test/fonts/Manrope-Bold.ttf',
]);
await _load('JetBrains Mono', const ['test/fonts/JetBrainsMono-Regular.ttf']);
// Noto Sans SC 子集(= 生产打包同一 client/fonts/ 子集):golden 中文用真字体渲染,
// 不出豆腐块、与真机一致(family 须匹配 PangolinFonts.cjk = 'Noto Sans SC')。
await _load('Noto Sans SC', const ['test/fonts/NotoSansSC-Regular-subset.otf']);
// Lucide 图标字体(family 须带 package 前缀以匹配 PangolinIcons 的 fontPackage)。
await _load('packages/lucide_icons/Lucide', const ['packages/lucide_icons_patched/fonts/lucide.ttf']);
await testMain();
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 65 KiB

@@ -91,6 +91,15 @@ class _FakeMeNotifier extends MeNotifier {
Future<Me> build() async => _demoMe;
}
// 固定语言的 locale notifier:localeProvider 是 StateNotifierProvider<LocaleNotifier,
// AppLang>,override 须返回 LocaleNotifier(子类)。构造后直接钉死 state 为目标语言
// (prefs 在测试态为空,父类 _load() 不会改写)。
class _FixedLocale extends LocaleNotifier {
_FixedLocale(AppLang lang) {
state = lang;
}
}
const Size _ipad = Size(1180, 820); // 对齐设计源 tabapp.jsx 内屏
Future<void> _shoot(
@@ -112,7 +121,7 @@ Future<void> _shoot(
tokenStoreProvider.overrideWithValue(const _NullTokenStore()),
vpnBridgeProvider.overrideWithValue(VpnBridgeMock()),
navViewProvider.overrideWith((ref) => view),
localeProvider.overrideWith((ref) => lang),
localeProvider.overrideWith((ref) => _FixedLocale(lang)),
nodesProvider.overrideWith(_FakeNodesNotifier.new),
meProvider.overrideWith(_FakeMeNotifier.new),
usageProvider((days: 30, device: null)).overrideWith((ref) async => _demoUsage),
@@ -89,12 +89,12 @@
Flutter 已很干净(UI 层零裸 hex),只需收尾。
- [x] 4.1 清 `client/lib/widgets/adaptive_menu.dart` 唯 1 处裸 Material 色 → 走 token
- [ ] 4.2 测试字体补 CJK 子集:用 `tools/fonts/make-cjk-subset.sh` 生成 Noto Sans SC 子集放
- [x] 4.2 测试字体补 CJK 子集:用 `tools/fonts/make-cjk-subset.sh` 生成 Noto Sans SC 子集放
`client/test/fonts/``flutter_test_config.dart` 注册——消除 golden 中文与生产渲染差异
- [ ] 4.3 处理现存 6 张 `client/test/golden/failures/` diff:逐张确认「原型对得上」后 `--update-goldens` 重录入库
- [x] 4.3 处理现存 6 张 `client/test/golden/failures/` diff:逐张确认「原型对得上」后 `--update-goldens` 重录入库
- [ ] 4.4 golden 覆盖扩容:desktop/tablet/mobile 全屏 × light/dark 双主题矩阵
(现有 `desktop_pages/tablet_pages/components/auth` → 补 mobile + 主题维度)
- [ ] 4.5 `client/test/helpers/harness.dart` 对齐 jiu `golden_harness.dart` 手法:
- [x] 4.5 `client/test/helpers/harness.dart` 对齐 jiu `golden_harness.dart` 手法:
多主题循环辅助 + 钉死 viewport/dpr + ProviderScope 固定数据(防动态值翻车)
---
+41 -47
View File
@@ -330,11 +330,11 @@ header .header-meta{color:var(--fg-soft)}
<div class="header-meta">生成于 2026-07-07 · 真相源 todo/todo.json</div>
<div class="stats">
<div class="stat-pill"><strong>18</strong>全部</div>
<div class="stat-pill"><strong>9</strong>待开始</div>
<div class="stat-pill"><strong>0</strong>开发中</div>
<div class="stat-pill"><strong>8</strong>待开始</div>
<div class="stat-pill"><strong>1</strong>开发中</div>
<div class="stat-pill"><strong>1</strong>待验收</div>
<div class="stat-pill"><strong>8</strong>已验收</div>
<div class="stat-pill gate-stat"><strong>1</strong>待确认</div>
</div>
</div>
</header>
@@ -376,7 +376,7 @@ header .header-meta{color:var(--fg-soft)}
<div class="section-block" id="section-open">
<div class="section-title st-open" data-toggle="open">
📋 待开始 <span class="s-count">9</span>
📋 待开始 <span class="s-count">8</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-open">
@@ -599,47 +599,6 @@ header .header-meta{color:var(--fg-soft)}
</div>
</li>
<li class="todo-card s-open"
data-id="19"
data-level="mid"
data-status="open"
data-tier="1"
data-tags="前端,Web,mac,iOS,Android,Windows,CI/CD">
<div class="card-header">
<span class="item-id">#19</span>
<span class="item-title">前端设计系统治理重构(ds-flow 全端)</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-1">一级</span>
</div>
</div>
<div class="item-desc">用 ds-flow 把 Flutter 五端+官网+用户中心收口到设计单源。补原型三件套(atoms.css/icons.js/index.html)+Web共享原子层去重(各自实现+同源闸)+硬编码色/fidelity闸+启用pre-commit。6阶段。计划见 docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</div>
<div class="gate-block gate-pending">
<div class="gate-head"><span class="gate-badge pending">⏸ 待确认</span>
<span class="gate-kind">一级方案规划 · 确认后方可继续开发</span></div>
<div class="gate-note">6 阶段 ds-flow 治理重构:Phase0 CLAUDE.md+计划落库 / Phase1 原型三件套(atoms.css/icons.js/index.html,收敛ui_kits) / Phase2 Web token同源闸 / Phase3 Web共享原子层去重(各自实现+同源闸,★工作量最大) / Phase4 Flutter收尾+golden补齐 / Phase5 静态闸挂满+启用pre-commit+fidelity体检。已定决策:Web各自实现+同源闸、ui_kits收敛纯HTML、先定稿再逐刀。主题保持light/dark。</div><div class="gate-ref">📄 详见 <code>docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</code></div>
<button class="approve-btn" data-id="19">确认执行 ✓</button>
<div class="approve-cmd" id="approve-cmd-19" style="display:none">
<div class="approve-cmd-label">在终端运行:</div>
<code class="approve-cmd-code">/todo approve 19</code>
<button class="approve-copy-btn" data-cmd="/todo approve 19">复制命令</button>
</div>
</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="CI/CD">CI/CD</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-07-07</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="3"
data-level="low"
@@ -675,12 +634,47 @@ header .header-meta{color:var(--fg-soft)}
</div>
<div class="section-block" id="section-doing">
<div class="section-title st-doing" data-toggle="doing">
🔨 开发中 <span class="s-count">0</span>
🔨 开发中 <span class="s-count">1</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-doing">
<ul class="todo-list" id="list-doing">
<p class="empty-tip">暂无条目</p>
<li class="todo-card s-doing"
data-id="19"
data-level="mid"
data-status="doing"
data-tier="1"
data-tags="前端,Web,mac,iOS,Android,Windows,CI/CD">
<div class="card-header">
<span class="item-id">#19</span>
<span class="item-title">前端设计系统治理重构(ds-flow 全端)</span>
<div class="card-badges">
<span class="tag status-badge s-doing">开发中</span>
<span class="tag t-high">重要</span>
<span class="tag tier-1">一级</span>
</div>
</div>
<div class="item-desc">用 ds-flow 把 Flutter 五端+官网+用户中心收口到设计单源。补原型三件套(atoms.css/icons.js/index.html)+Web共享原子层去重(各自实现+同源闸)+硬编码色/fidelity闸+启用pre-commit。6阶段。计划见 docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</div>
<div class="gate-block gate-granted">
<div class="gate-head"><span class="gate-badge granted">✓ 已确认</span>
<span class="gate-kind">一级方案规划</span>
<span class="gate-date">2026-07-07</span></div>
<div class="gate-note">6 阶段 ds-flow 治理重构:Phase0 CLAUDE.md+计划落库 / Phase1 原型三件套(atoms.css/icons.js/index.html,收敛ui_kits) / Phase2 Web token同源闸 / Phase3 Web共享原子层去重(各自实现+同源闸,★工作量最大) / Phase4 Flutter收尾+golden补齐 / Phase5 静态闸挂满+启用pre-commit+fidelity体检。已定决策:Web各自实现+同源闸、ui_kits收敛纯HTML、先定稿再逐刀。主题保持light/dark。</div><div class="gate-ref">📄 详见 <code>docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</code></div>
</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="CI/CD">CI/CD</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-07-07</span>
</div>
</div>
</li>
</ul>
</div>
</div>
+4 -4
View File
@@ -1,7 +1,7 @@
{
"meta": {
"title": "feature+windows — 项目 TODO",
"updated_at": "2026-07-07T15:41:36.537Z"
"updated_at": "2026-07-07T15:54:54.457Z"
},
"seq": 19,
"items": [
@@ -304,7 +304,7 @@
"CI/CD"
],
"owner": "agent",
"status": "open",
"status": "doing",
"created_at": "2026-07-07T15:41:25.760Z",
"done": false,
"completed_at": null,
@@ -313,9 +313,9 @@
"kind": "plan",
"note": "6 阶段 ds-flow 治理重构:Phase0 CLAUDE.md+计划落库 / Phase1 原型三件套(atoms.css/icons.js/index.html,收敛ui_kits) / Phase2 Web token同源闸 / Phase3 Web共享原子层去重(各自实现+同源闸,★工作量最大) / Phase4 Flutter收尾+golden补齐 / Phase5 静态闸挂满+启用pre-commit+fidelity体检。已定决策:Web各自实现+同源闸、ui_kits收敛纯HTML、先定稿再逐刀。主题保持light/dark。",
"ref": "docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md",
"approval": "pending",
"approval": "granted",
"proposed_at": "2026-07-07T15:41:36.536Z",
"approved_at": null
"approved_at": "2026-07-07T15:54:54.386Z"
}
}
]