Ver código fonte

test(family): S-cfc-38 family.invite.joinRequestCardInfo RED contract tests (WI-4 审批卡片三项信息) + vue SFC jest transform

E2E Test Bot 5 dias atrás
pai
commit
c5e065dda

+ 105 - 0
cfc-frontend/__tests__/pages/family-members.joinRequestCardInfo.test.js

@@ -0,0 +1,105 @@
+/**
+ * Contract tests for capability key `family.invite.joinRequestCardInfo`
+ * (spec: docs/superpowers/specs/2026-11-07-family-join-required-info.md §5 WI-4)
+ *
+ * AC4.1  待审批卡片显示申请人真实姓名、出生日期、性别。
+ * AC4.2  任一字段缺失时优雅回退(不报错、不显示乱码)。
+ *
+ * TDD phase: RED. These tests target the REAL `family-members.vue` join-request
+ * card. The current implementation only renders the requester's name
+ * (`requesterRealName || requesterNickname`); it does NOT render the applicant's
+ * birthday or gender. Therefore the AC4.1 assertions are expected to FAIL until
+ * WI-4 is implemented (approval card shows birthday + gender).
+ *
+ * Note: `@jest-environment jsdom` is required because @vue/test-utils `mount`
+ * needs a DOM; the default `node` env does not provide one.
+ *
+ * @jest-environment jsdom
+ */
+import { mount } from '@vue/test-utils'
+import FamilyMembers from '@/pages/profile-extra/family-members.vue'
+
+// A fully-populated pending join request (FamilyJoinRequestDTO shape).
+const fullRequest = {
+  id: 1001,
+  requesterUserId: 200,
+  requesterRealName: '张三',
+  requesterNickname: 'zhangsan',
+  requesterBirthday: '1990-01-01',
+  requesterGender: 'male',
+  requesterAvatar: '/static/avatar.jpg'
+}
+
+async function mountWithRequests(requests) {
+  const wrapper = mount(FamilyMembers, {
+    stubs: { 'generation-picker': true }
+  })
+  wrapper.setData({ joinRequests: requests || [] })
+  await wrapper.vm.$nextTick()
+  return wrapper
+}
+
+describe('family.invite.joinRequestCardInfo (WI-4 审批卡片展示三项信息)', () => {
+  it('AC4.1: 审批卡片显示申请人真实姓名、出生日期、性别', async () => {
+    const wrapper = await mountWithRequests([fullRequest])
+
+    const section = wrapper.find('.join-request-section')
+    expect(section.exists()).toBe(true)
+
+    const text = section.text()
+    // 真实姓名(AC4.1 第一项)
+    expect(text).toContain('张三')
+    // 出生日期(AC4.1 第二项)
+    expect(text).toContain('1990-01-01')
+    // 性别(AC4.1 第三项):male/female 或其中文标签 男/女 均可
+    expect(text).toMatch(/(male|female|男|女)/)
+  })
+
+  it('AC4.1: 性别为 female 时卡片仍展示性别信息', async () => {
+    const wrapper = await mountWithRequests([
+      Object.assign({}, fullRequest, { requesterGender: 'female', requesterRealName: '李四', requesterBirthday: '1985-06-15' })
+    ])
+    const text = wrapper.find('.join-request-section').text()
+    expect(text).toContain('李四')
+    expect(text).toContain('1985-06-15')
+    expect(text).toMatch(/(female|男|女)/)
+  })
+
+  it('AC4.2: 出生日期/性别缺失时不报错、不显示 undefined/null 乱码,且真实姓名仍展示', async () => {
+    const partialRequest = {
+      id: 1002,
+      requesterUserId: 201,
+      requesterRealName: '王五',
+      requesterNickname: 'wangwu'
+      // requesterBirthday / requesterGender 缺失(null/undefined)
+    }
+    // 组件应正常挂载渲染(抛错即失败)
+    const wrapper = await mountWithRequests([partialRequest])
+
+    const text = wrapper.find('.join-request-section').text()
+    expect(text).toContain('王五')
+    // 优雅回退:不允许把缺失值渲染成字面 'undefined' / 'null'
+    expect(text).not.toContain('undefined')
+    expect(text).not.toContain('null')
+  })
+
+  it('AC4.2: 真实姓名缺失时回退展示昵称(不显示空白/乱码)', async () => {
+    const noRealName = {
+      id: 1003,
+      requesterUserId: 202,
+      requesterRealName: '',
+      requesterNickname: 'nick_fallback',
+      requesterBirthday: '2000-12-31',
+      requesterGender: 'female'
+    }
+    const wrapper = await mountWithRequests([noRealName])
+    const text = wrapper.find('.join-request-section').text()
+    expect(text).toContain('nick_fallback')
+    expect(text).not.toContain('undefined')
+  })
+
+  it('边界: 无待处理加入申请时不渲染审批卡片区块', async () => {
+    const wrapper = await mountWithRequests([])
+    expect(wrapper.find('.join-request-section').exists()).toBe(false)
+  })
+})

+ 57 - 0
cfc-frontend/__tests__/vue-transform.js

@@ -0,0 +1,57 @@
+/**
+ * Jest transformer for uni-app `.vue` Single File Components (Vue 2.7).
+ *
+ * PURPOSE: cfc-frontend's default jest.config.js only transforms `.js` via
+ * babel-jest, so `.vue` SFCs cannot be imported by tests. This transformer
+ * compiles the SFC `<script>` block (Options API) to CommonJS and precompiles
+ * the `<template>` block to a Vue 2 render function (the default `vue` entry
+ * in Node is runtime-only and has no template compiler), so tests can `mount`
+ * real page components with `@vue/test-utils`.
+ *
+ * TEST-ONLY infrastructure: this file lives under `__tests__/` and is NOT part
+ * of the app runtime. It does not modify any `pages/` / `utils/` business code.
+ */
+const { parseComponent, compileTemplate } = require('@vue/compiler-sfc')
+const babel = require('@babel/core')
+
+module.exports = {
+  process(src, filename) {
+    const descriptor = parseComponent(src, { filename })
+    if (descriptor.errors && descriptor.errors.length) {
+      throw new Error('[vue-transform] SFC parse error in ' + filename + ':\n' + descriptor.errors.join('\n'))
+    }
+
+    let code = ''
+    if (descriptor.script && descriptor.script.content) {
+      // Options API: `export default {...}` -> `const __sfc_main = {...}`
+      code += descriptor.script.content.replace(/export\s+default/, 'const __sfc_main =')
+    } else {
+      code += 'const __sfc_main = {}'
+    }
+
+    if (descriptor.template && descriptor.template.content) {
+      const res = compileTemplate({
+        source: descriptor.template.content,
+        filename,
+        id: filename.replace(/[^\w]/g, '_')
+      })
+      if (res.errors && res.errors.length) {
+        throw new Error('[vue-transform] template compile error in ' + filename + ':\n' + res.errors.join('\n'))
+      }
+      code += '\n' + res.code
+      code += '\n__sfc_main.render = render'
+      code += '\n__sfc_main.staticRenderFns = staticRenderFns'
+    }
+
+    code += '\nmodule.exports = __sfc_main'
+    // Transpile the assembled module to CommonJS (the <script> block keeps ES
+    // `import` statements; Node cannot execute them in a jest CJS module).
+    const out = babel.transformSync(code, {
+      filename,
+      presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
+      babelrc: false,
+      configFile: false
+    })
+    return { code: out.code, map: null }
+  }
+}

+ 3 - 1
cfc-frontend/jest.config.js

@@ -1,7 +1,9 @@
 module.exports = {
   moduleFileExtensions: ['js', 'json'],
   transform: {
-    '^.+\\.js$': 'babel-jest'
+    '^.+\\.js$': 'babel-jest',
+    // .vue SFC transform (test-only infra, see __tests__/vue-transform.js)
+    '^.+\\.vue$': '<rootDir>/__tests__/vue-transform.js'
   },
   moduleNameMapper: {
     '^@/(.*)$': '<rootDir>/$1'