Просмотр исходного кода

test(cfc-frontend): achievement.grid.fiveCol 契约级单测 S-cfc-32

Sisyphus Agent 2 дней назад
Родитель
Сommit
b6c3896c3e
1 измененных файлов с 114 добавлено и 0 удалено
  1. 114 0
      cfc-frontend/__tests__/pages/profile-main/achievement.fiveCol.test.js

+ 114 - 0
cfc-frontend/__tests__/pages/profile-main/achievement.fiveCol.test.js

@@ -0,0 +1,114 @@
+/**
+ * 契约级单测(TDD-RED)——能力键 achievement.grid.fiveCol
+ *
+ * 溯源:
+ *   requirement: tests/user-scenarios/应用场景总览.md SC-A-27(成就与成长历史·家长端)
+ *                + docs/需求分析/2026-11-06-成就成长打卡需求变更.md 变更 D(我的成就固定五列)
+ *   design:      cfc-frontend/pages/profile-main/profile.vue「我的成就」区块
+ *                模板 L151-187(.achievement-grid.cols-5 + 5 个 .ach-item)+
+ *                样式 原 .ach-item{width:25%} 改五列等分 .achievement-grid.cols-5 .ach-item{width:20%}
+ *
+ * 缺陷本质(本用例守护的规则):
+ *   「我的成就」区块固定 5 张成就卡(连续打卡/完成任务/收集勋章/成长记录/参加挑战)。
+ *   原 .ach-item{width:25%}(4 列等分)→ 第 5 张卡折行错位。
+ *   变更 D 要求该区块独占 5 列等分(width:20%),单行排布不折行;
+ *   且必须用独立网格类(.cols-5)隔离,不得全局改 .achievement-grid(其被「我的团队」.team-grid / 「卡券中心」复用)。
+ *
+ * 断言口径(静态结构契约,读真实 SFC 源码,不依赖渲染引擎):
+ *   1) 模板「我的成就」区块根容器使用 .achievement-grid.cols-5
+ *   2) 样式存在 .achievement-grid.cols-5 .ach-item 规则,且 width 为 20%(五列等分)
+ *   3) 该区块含 ≥5 个成就入口,且跳转方法为 goToStreak/goToTaskHistory/goToBadges/goToGrowth/goToChallenges
+ *   4) 隔离性:全局 .achievement-grid 未被改成 20%(保护「我的团队」3 列布局不被波及)
+ *
+ * 说明(既有 Jest 体系约束):
+ *   cfc-frontend jest.config.js 仅 transform .js,未装 vue-jest/jsdom 组件环境——
+ *   沿用既有约定:读取真实 SFC 源码做结构契约断言(参考 __tests__/family-join/join-form.required-info.test.js、
+ *   __tests__/pages/growth-main/growthGrid.layout.test.js)。本文件不修改 pages/ 任何业务代码。
+ */
+
+import fs from 'fs'
+import path from 'path'
+
+const PAGE_PATH = path.join(__dirname, '../../../pages/profile-main/profile.vue')
+const SRC = fs.readFileSync(PAGE_PATH, 'utf8')
+
+/** 截取某个 CSS 选择器的声明块({} 内文本),返回 null 表示不存在 */
+function cssBlock(src, selector) {
+  const idx = src.indexOf('\n' + selector + ' {')
+  const altIdx = src.indexOf(selector + ' {')
+  const start = idx >= 0 ? idx : altIdx
+  if (start < 0) return null
+  const open = src.indexOf('{', start)
+  const close = src.indexOf('}', open)
+  if (open < 0 || close < 0) return null
+  return src.slice(open + 1, close)
+}
+
+/** 截取模板中「我的成就」section 块(class="achievement-section" 且其内含「我的成就」) */
+function achievementSectionTemplate(src) {
+  const tmpl = src.match(/<template>([\s\S]*?)<\/template>/)
+  if (!tmpl) return ''
+  const t = tmpl[1]
+  // 成就区块以 class="achievement-section" 起始,逐段切分找到含「我的成就」的那段
+  const marker = 'achievement-section'
+  let from = 0
+  while (true) {
+    const secStart = t.indexOf(marker, from)
+    if (secStart < 0) return ''
+    const secEnd = t.indexOf(marker, secStart + 1)
+    const chunk = t.slice(secStart, secEnd > 0 ? secEnd : t.length)
+    if (chunk.indexOf('我的成就') >= 0) return chunk
+    from = secStart + 1
+  }
+}
+
+// ---------------------------------------------------------------------------
+// 变更 D · 我的成就固定五列
+// ---------------------------------------------------------------------------
+
+describe('achievement.grid.fiveCol · 我的成就固定五列(变更 D / SC-A-27)', () => {
+  test('TC-A5-01 模板「我的成就」区块使用 .achievement-grid.cols-5 根容器', () => {
+    const section = achievementSectionTemplate(SRC)
+    expect(section).not.toBe('')
+    expect(section).toMatch(/achievement-grid\s+cols-5/)
+  })
+
+  test('TC-A5-02 .achievement-grid.cols-5 .ach-item 宽度为 20%(五列等分)', () => {
+    const block = cssBlock(SRC, '.achievement-grid.cols-5 .ach-item')
+    expect(block).not.toBeNull()
+    const m = /width:\s*([\d.]+)%/.exec(block)
+    expect(m).not.toBeNull()
+    expect(Number(m[1])).toBe(20)
+  })
+
+  test('TC-A5-03 成就区块含 ≥5 个 .ach-item 成就入口,且不折行错位(5 张卡单行)', () => {
+    const section = achievementSectionTemplate(SRC)
+    const items = section.match(/class="ach-item"/g) || []
+    expect(items.length).toBeGreaterThanOrEqual(5)
+  })
+
+  test('TC-A5-04 5 个成就入口跳转方法齐备(goToStreak/TaskHistory/Badges/Growth/Challenges)', () => {
+    const section = achievementSectionTemplate(SRC)
+    const required = ['goToStreak', 'goToTaskHistory', 'goToBadges', 'goToGrowth', 'goToChallenges']
+    required.forEach((fn) => {
+      // 每个成就卡的 @click 绑定到对应方法
+      expect(section).toMatch(new RegExp('@click="' + fn + '"'))
+    })
+  })
+
+  test('TC-A5-05 隔离性:全局 .achievement-grid 未被改成 20%(保护「我的团队」布局)', () => {
+    // 全局 .achievement-grid 仅声明 flex 布局,不含 width:20%;团队网格用 33.33%
+    const globalGrid = cssBlock(SRC, '.achievement-grid')
+    expect(globalGrid).not.toBeNull()
+    expect(globalGrid).not.toMatch(/width:\s*20%/)
+  })
+})
+
+// ---------------------------------------------------------------------------
+// 权限流:本能力为家长端「我的成就」展示布局,无角色分支门禁,
+// 属《编排派发》中「权限(适用时)」的不适用项,显式记录。
+// ---------------------------------------------------------------------------
+
+describe.skip('权限流:成就五列布局无角色门禁,不适用', () => {
+  it('占位:该能力面向家长端个人中心展示,无 role 分支(详见汇报)', () => {})
+})