| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- /**
- * 【API级】家庭成员管理全流程
- * ================================================================
- * 对应 DOM 版:e2e/family-member-management.spec.js(远程无 SPA,DOM 版不可跑)
- * 接口契约来源:FamilyMembersController(/api/family/member/*)
- * add : { generationLevel, nickname, gender?, birthday?, relativeMemberId?, peerType? }
- * list : { familyId? | visibleOnly? | includeFamily? } 越权 → code=403
- * switch : { memberId } 切换到自己 → 「不能切换到自己」
- * kick : { memberId } 仅家庭创建者可踢
- * update : { memberId, nickname?, generationLevel?, ... }
- * 运行:npx playwright test -c playwright.config.api.js --project=desktop
- */
- const { test, expect } = require('@playwright/test');
- const { registerRole, apiPost, pickMemberByUserId, uniqueSuffix } = require('../helpers/api');
- test.describe('【API级】家庭成员管理全流程', () => {
- let ctx;
- test.beforeAll(async ({ request }) => {
- ctx = await registerRole(request, 'parent', 'QA-FAMILY');
- });
- /** 创建一个独立的手动成员(非微信登录用户),返回 { id, nickname } */
- async function createManualChild(request, tag) {
- const nickname = 'QA_' + tag + '_' + uniqueSuffix();
- const r = await apiPost(
- request,
- '/api/family/member/add',
- { generationLevel: 'child', nickname, gender: 'male', birthday: '2015-06-15' },
- ctx.token
- );
- expect(r.code, '添加成员应成功').toBe(200);
- expect(r.data && r.data.id, '应返回成员ID').toBeTruthy();
- return { id: r.data.id, nickname: r.data.nickname || nickname };
- }
- // ===== 场景1:成员列表(关系/成员数据可获取)=====
- test('[场景1] 获取成员列表 - 期望返回成员数据', async ({ request }) => {
- const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- expect(r.code).toBe(200);
- expect(Array.isArray(r.data)).toBeTruthy();
- expect(r.data.length).toBeGreaterThan(0);
- });
- test('[场景1b] 越权访问他人家庭 - 期望 403 拒绝', async ({ request }) => {
- const r = await apiPost(request, '/api/family/member/list', { familyId: 999999999 }, ctx.token);
- expect(r.code).toBe(403);
- expect(String(r.message)).toContain('无权访问');
- });
- // ===== 场景2:添加新家庭成员 =====
- test('[场景2] 添加新家庭成员 - 期望添加成功并返回完整信息', async ({ request }) => {
- const m = await createManualChild(request, 'ADD');
- const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- expect(r.code).toBe(200);
- const hit = r.data.find((x) => String(x.id) === String(m.id));
- expect(hit, '新增成员应出现在成员列表中').toBeTruthy();
- expect(hit.nickname).toContain('QA_ADD');
- });
- test('[场景2b] 添加成员时昵称为空 - 期望返回错误提示', async ({ request }) => {
- const r = await apiPost(
- request,
- '/api/family/member/add',
- { generationLevel: 'child', nickname: '' },
- ctx.token
- );
- expect(r.code).not.toBe(200);
- expect(String(r.message)).toContain('昵称');
- });
- test('[场景2c] 添加成员时辈分等级为空 - 期望返回错误提示', async ({ request }) => {
- const r = await apiPost(
- request,
- '/api/family/member/add',
- { nickname: 'QA_NO_GEN_' + uniqueSuffix() },
- ctx.token
- );
- expect(r.code).not.toBe(200);
- expect(String(r.message)).toContain('辈分等级');
- });
- test('[场景2d] 添加成员时辈分等级非法 - 期望返回错误提示', async ({ request }) => {
- const r = await apiPost(
- request,
- '/api/family/member/add',
- { generationLevel: 'not_a_level', nickname: 'QA_BAD_GEN_' + uniqueSuffix() },
- ctx.token
- );
- expect(r.code).not.toBe(200);
- expect(String(r.message)).toContain('辈分等级');
- });
- // ===== 场景3:查看家庭成员列表 =====
- test('[场景3] 查看家庭成员列表 - 期望返回所有成员及关系信息', async ({ request }) => {
- const m = await createManualChild(request, 'LIST');
- const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- expect(r.code).toBe(200);
- expect(r.data.length).toBeGreaterThan(1);
- const hit = r.data.find((x) => String(x.id) === String(m.id));
- expect(hit).toBeTruthy();
- // 关系信息应随成员一并返回(FamilyMemberVO: relationship / roleLabel / relativeLabel / generation)
- expect(
- 'relationship' in hit || 'roleLabel' in hit || 'relativeLabel' in hit || 'generation' in hit
- ).toBeTruthy();
- expect(hit.relationship, '应返回关系名称').toBeTruthy();
- });
- // ===== 场景4:更新成员信息 =====
- /**
- * 【已知缺陷 CFC-FM-001】远程环境下 /api/family/member/update 更新 nickname
- * 返回 code=200 但昵称未变更(gender/birthday 可正常更新),属字段级不生效缺陷;
- * 本地源码 FamilyMemberService.updateMember 已含 nickname 分支,
- * 疑似远程部署构建落后于当前源码,需研发核对版本。
- */
- test('[场景4] 更新成员关系/昵称 - 期望更新成功并返回新信息', async ({ request }) => {
- test.fixme(true, 'CFC-FM-001:member/update 更新 nickname 返回成功但未生效,修复后移除本标记');
- const m = await createManualChild(request, 'UPD');
- const newName = 'QA_UPDATED_' + uniqueSuffix();
- const r = await apiPost(
- request,
- '/api/family/member/update',
- { memberId: m.id, nickname: newName },
- ctx.token
- );
- expect(r.code, '更新成员应成功').toBe(200);
- expect(r.data).toBeTruthy();
- const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- const hit = list.data.find((x) => String(x.id) === String(m.id));
- expect(hit.nickname).toBe(newName);
- });
- test('[场景4c] 已知缺陷复现 - nickname 未生效但 gender/birthday 生效', async ({ request }) => {
- // 特征化用例:锁定当前(缺陷)行为;研发修复后本用例会失败,提示移除 场景4 的 fixme
- const m = await createManualChild(request, 'UPDC');
- const newName = 'QA_NICK_' + uniqueSuffix();
- const upd = await apiPost(
- request,
- '/api/family/member/update',
- { memberId: m.id, nickname: newName, gender: 'female', birthday: '2017-05-05' },
- ctx.token
- );
- expect(upd.code).toBe(200);
- expect(upd.data.nickname, 'CFC-FM-001 当前表现为昵称未变更').toBe(m.nickname);
- // 对照组:其它字段可正常更新
- expect(upd.data.gender).toBe('female');
- expect(String(upd.data.birthday)).toContain('2017-05-05');
- });
- // ===== 场景5:切换到成员视角 =====
- test('[场景5] 切换到成员视角 - 期望返回切换凭证', async ({ request }) => {
- const m = await createManualChild(request, 'SW');
- const r = await apiPost(request, '/api/family/member/switch', { memberId: m.id }, ctx.token);
- expect(r.code, '切换到手动成员应成功').toBe(200);
- expect(r.data).toBeTruthy();
- expect(String(r.data.memberId)).toBe(String(m.id));
- expect(r.data).toHaveProperty('effectiveRole');
- });
- test('[场景5b] 切换到本人视角 - 期望返回错误提示', async ({ request }) => {
- const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- const self = pickMemberByUserId(list.data, ctx.userId);
- expect(self, '成员列表应包含登录用户自身').toBeTruthy();
- const r = await apiPost(request, '/api/family/member/switch', { memberId: self.id }, ctx.token);
- expect(r.code).not.toBe(200);
- expect(String(r.message)).toContain('不能切换到自己');
- });
- // ===== 场景6:移除家庭成员 =====
- test('[场景6] 移除家庭成员 - 期望成员从家庭中移除', async ({ request }) => {
- const m = await createManualChild(request, 'KICK');
- const before = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- const beforeCount = before.data.length;
- const r = await apiPost(request, '/api/family/member/kick', { memberId: m.id }, ctx.token);
- expect(r.code, '踢出成员应成功').toBe(200);
- const after = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- expect(after.data.length).toBeLessThan(beforeCount);
- expect(after.data.find((x) => String(x.id) === String(m.id))).toBeFalsy();
- });
- test('[场景6b] 非管理员尝试踢人 - 期望权限不足拒绝', async ({ request }) => {
- const target = await createManualChild(request, 'PERM');
- const other = await registerRole(request, 'parent', 'QA-OTHER');
- const r = await apiPost(request, '/api/family/member/kick', { memberId: target.id }, other.token);
- expect(r.code).not.toBe(200);
- });
- // ===== 场景7:完整家庭成员管理流程(端到端)=====
- test('[场景7] 完整家庭成员管理流程 - 期望全流程成功', async ({ request }) => {
- // 1) 添加成员
- const nickname = 'QA_E2E_' + uniqueSuffix();
- const add = await apiPost(
- request,
- '/api/family/member/add',
- { generationLevel: 'child', nickname, gender: 'female', birthday: '2016-03-08' },
- ctx.token
- );
- expect(add.code).toBe(200);
- const memberId = add.data.id;
- // 2) 列表可见
- const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- expect(list.data.find((x) => String(x.id) === String(memberId))).toBeTruthy();
- // 3) 更新成员
- const updated = 'QA_E2E_UPD_' + uniqueSuffix();
- const upd = await apiPost(request, '/api/family/member/update', { memberId, nickname: updated }, ctx.token);
- expect(upd.code).toBe(200);
- // 4) 切换视角
- const sw = await apiPost(request, '/api/family/member/switch', { memberId }, ctx.token);
- expect(sw.code).toBe(200);
- // 5) 移除成员
- const kick = await apiPost(request, '/api/family/member/kick', { memberId }, ctx.token);
- expect(kick.code).toBe(200);
- // 6) 终态校验:成员已不在列表
- const finalList = await apiPost(request, '/api/family/member/list', {}, ctx.token);
- expect(finalList.data.find((x) => String(x.id) === String(memberId))).toBeFalsy();
- });
- });
|