|
|
@@ -0,0 +1,186 @@
|
|
|
+/**
|
|
|
+ * 契约级单测(TDD-RED)——能力键 family.signature.consignee
|
|
|
+ *
|
|
|
+ * 溯源:
|
|
|
+ * requirement: docs/superpowers/specs/2026-11-07-signature-page-landscape.md
|
|
|
+ * §3 AC-R3 / §5 WI-4(水机购买签名改造:收件人信息段 + 手签名段)
|
|
|
+ * design: cfc-frontend/pages/signature/components/consignee-edit.vue(签收人信息 + 手签名段组件)
|
|
|
+ *
|
|
|
+ * 说明(既有 Jest 体系约束):
|
|
|
+ * cfc-frontend 的 jest.config.js 仅 transform .js(babel-jest),moduleFileExtensions 不含 .vue,
|
|
|
+ * 且未安装 vue-jest/jsdom 组件环境 —— 既有约定为「抽取被测逻辑断言」而非挂载 .vue
|
|
|
+ * (见 __tests__/components/FamilyRelationGraph.test.js)。
|
|
|
+ * 本文件沿用 join-form.required-info.test.js 的风格:直接加载真实 SFC 的 <script>/<template> 源码——
|
|
|
+ * - <script>:把 import 语句替换为注入桩后求值,得到真实组件的 data()/methods,做行为级断言;
|
|
|
+ * - <template>:读取模板文本,做结构契约断言(收件人姓名/电话/地址字段、手签名确认段、确认回调)。
|
|
|
+ *
|
|
|
+ * 当前状态:组件 pages/signature/components/consignee-edit.vue 尚未落地(实现未交付)→ 断言文件存在性 → 预期 RED。
|
|
|
+ * 实现方在 GREEN 阶段按本文件所锚定的契约补足组件后,断言应可翻绿。
|
|
|
+ * 本文件不修改 src/main、不修改任何既有测试。
|
|
|
+ */
|
|
|
+
|
|
|
+import fs from 'fs'
|
|
|
+import path from 'path'
|
|
|
+
|
|
|
+// 测试位于 __tests__/pages/signature/,组件位于 pages/signature/components/ → 需三级回退到 cfc-frontend
|
|
|
+const CONSIGNEE_EDIT_PATH = path.join(__dirname, '../../../pages/signature/components/consignee-edit.vue')
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 工具:加载真实 SFC 的 <script> 逻辑(行为级)与 <template> 结构(结构级)
|
|
|
+// 风格对齐 cfc-frontend/__tests__/family-join/join-form.required-info.test.js
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+/** 把 ES import 语句替换为从 globalThis.__consigneeEditStubs 取桩的声明(不解析模块,避免污染业务实现) */
|
|
|
+function stubImports(script, stubMap) {
|
|
|
+ return script.replace(/import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"];?/g, function (m, clause, id) {
|
|
|
+ var names = []
|
|
|
+ var c = clause.trim()
|
|
|
+ var braceIdx = c.indexOf('{')
|
|
|
+ if (braceIdx >= 0) {
|
|
|
+ var def = c.slice(0, braceIdx).replace(/,\s*$/, '').trim()
|
|
|
+ if (def) names.push(def)
|
|
|
+ var named = c.slice(braceIdx + 1, c.lastIndexOf('}'))
|
|
|
+ named.split(',').forEach(function (part) {
|
|
|
+ var p = part.trim()
|
|
|
+ if (!p) return
|
|
|
+ var parts = p.split(/\s+as\s+/)
|
|
|
+ var local = parts.length > 1 ? parts[1].trim() : parts[0].trim()
|
|
|
+ if (local) names.push(local)
|
|
|
+ })
|
|
|
+ } else if (c.indexOf('*') === 0) {
|
|
|
+ var nsMatch = c.match(/\*\s+as\s+(\w+)/)
|
|
|
+ if (nsMatch) names.push(nsMatch[1])
|
|
|
+ } else {
|
|
|
+ names.push(c)
|
|
|
+ }
|
|
|
+ return names.map(function (n) {
|
|
|
+ return 'var ' + n + ' = (globalThis.__consigneeEditStubs[' + JSON.stringify(id) + '] || {})[' + JSON.stringify(n) + '];'
|
|
|
+ }).join('\n')
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/** 加载真实 SFC:返回 { component, template }(component = 真实 <script> 的 export default 对象);文件缺失时抛出明确错误 */
|
|
|
+function loadConsigneeEdit() {
|
|
|
+ if (!fs.existsSync(CONSIGNEE_EDIT_PATH)) {
|
|
|
+ throw new Error('组件文件尚未实现(GREEN 未交付):' + CONSIGNEE_EDIT_PATH)
|
|
|
+ }
|
|
|
+ var src = fs.readFileSync(CONSIGNEE_EDIT_PATH, 'utf8')
|
|
|
+ var tmplMatch = src.match(/<template>([\s\S]*?)<\/template>/)
|
|
|
+ var scriptMatch = src.match(/<script>([\s\S]*?)<\/script>/)
|
|
|
+ if (!scriptMatch) throw new Error('无法在 consignee-edit.vue 中定位 <script> 块')
|
|
|
+ var script = stubImports(scriptMatch[1], globalThis.__consigneeEditStubs || {})
|
|
|
+ script = script.replace(/export default\s*\{/, 'module.exports = {')
|
|
|
+ var mod = { exports: {} }
|
|
|
+ // eslint-disable-next-line no-new-func
|
|
|
+ var fn = new Function('module', 'exports', script)
|
|
|
+ fn(mod, mod.exports)
|
|
|
+ return { component: mod.exports, template: tmplMatch ? tmplMatch[1] : '' }
|
|
|
+}
|
|
|
+
|
|
|
+/** 依据真实组件的 data()/methods 构造可驱动上下文(模拟 Vue 把 data 与 methods 合并到实例) */
|
|
|
+function instantiate(component) {
|
|
|
+ var vm = {}
|
|
|
+ var data = typeof component.data === 'function' ? component.data.call(vm) : (component.data || {})
|
|
|
+ Object.keys(data).forEach(function (k) { vm[k] = data[k] })
|
|
|
+ var methods = component.methods || {}
|
|
|
+ Object.keys(methods).forEach(function (k) { vm[k] = methods[k] })
|
|
|
+ return vm
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 测试夹具(桩表:为 GREEN 阶段组件可能存在的 import 预留,缺失时按可选访问处理)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+var stubs
|
|
|
+
|
|
|
+beforeEach(function () {
|
|
|
+ global.uni.showToast.mockReset()
|
|
|
+ global.uni.navigateBack.mockReset()
|
|
|
+ global.uni.request.mockReset()
|
|
|
+ stubs = {
|
|
|
+ getConsigneeData: jest.fn(function () {
|
|
|
+ return Promise.resolve({ name: '张三', phone: '13800001111', address: '北京市朝阳区' })
|
|
|
+ }),
|
|
|
+ onConfirm: jest.fn(function (dataUrl) { return dataUrl }),
|
|
|
+ onAgreeChange: jest.fn(function (e) { return e })
|
|
|
+ }
|
|
|
+ globalThis.__consigneeEditStubs = {
|
|
|
+ '@/utils/api.js': { getConsigneeData: stubs.getConsigneeData }
|
|
|
+ }
|
|
|
+ jest.useFakeTimers()
|
|
|
+})
|
|
|
+
|
|
|
+afterEach(function () {
|
|
|
+ jest.useRealTimers()
|
|
|
+ delete globalThis.__consigneeEditStubs
|
|
|
+})
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// AC-R0 组件文件存在性契约(本能力 TDD-RED:实现未落地 → 文件应缺失 → 本组断言红)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+describe('AC-R0 组件文件存在性契约', function () {
|
|
|
+ it('pages/signature/components/consignee-edit.vue 必须存在', function () {
|
|
|
+ expect(fs.existsSync(CONSIGNEE_EDIT_PATH)).toBe(true)
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// AC-R1 <script> 契约:组件导出收件人信息编辑的 data()/methods(GREEN 后强制加载真实源码断言)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+describe('AC-R1 <script> 契约:收件人信息 + 手签名确认回调', function () {
|
|
|
+ var component
|
|
|
+
|
|
|
+ beforeEach(function () {
|
|
|
+ // 文件缺失时 loadConsigneeEdit 抛错 → 每条用例红(明确的 RED 信号);GREEN 后加载真实 SFC 断言
|
|
|
+ component = loadConsigneeEdit().component
|
|
|
+ })
|
|
|
+
|
|
|
+ it('组件必须定义收件人信息字段(姓名/电话/地址)', function () {
|
|
|
+ var vm = instantiate(component)
|
|
|
+ expect(vm.consigneeName).toBeDefined()
|
|
|
+ expect(vm.consigneePhone).toBeDefined()
|
|
|
+ expect(vm.consigneeAddress).toBeDefined()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('组件必须暴露确认回调方法(手签名确认后写回收件人信息)', function () {
|
|
|
+ expect(typeof component.methods.onConfirm).toBe('function')
|
|
|
+ component.methods.onConfirm.call(instantiate(component), 'data:image/png;base64,xxxx')
|
|
|
+ expect(stubs.onConfirm).toHaveBeenCalled()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// AC-R2 <template> 契约:签收人信息输入段 + 手签名确认段 + 确认回调绑定
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+describe('AC-R2 <template> 契约:签收人信息段 + 手签名确认段', function () {
|
|
|
+ var template
|
|
|
+
|
|
|
+ beforeEach(function () {
|
|
|
+ template = loadConsigneeEdit().template
|
|
|
+ })
|
|
|
+
|
|
|
+ it('模板包含收件人姓名/电话/地址输入绑定', function () {
|
|
|
+ expect(template).toMatch(/consigneeName/)
|
|
|
+ expect(template).toMatch(/consigneePhone/)
|
|
|
+ expect(template).toMatch(/consigneeAddress/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('模板包含手签名确认段(内嵌 HandSignature 组件)', function () {
|
|
|
+ expect(template).toMatch(/<hand-signature/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('模板将确认事件绑定到确认回调(@confirm="onConfirm")', function () {
|
|
|
+ expect(template).toMatch(/@confirm\s*=\s*["']onConfirm["']/)
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 权限流:水机购买收件人信息编辑属于已登录购物流程,无角色门禁分支(不适用项,显式记录)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+describe.skip('权限流:收件人信息编辑无角色门禁,不适用', function () {
|
|
|
+ it('占位:该能力面向已登录购买流程,无 role 分支(详见汇报)', function () {})
|
|
|
+})
|