|
|
@@ -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 分支(详见汇报)', () => {})
|
|
|
+})
|