fix(member): 修复退出登录后可返回旧页面的问题
1. 退出登录和注销流程兜底分支都改用reLaunch跳转登录页,清空页面栈防止返回旧会员页 2. 新增完整的单元测试覆盖logout和getCancelStatus的异常分支、调用顺序和跳转模式
This commit is contained in:
@@ -17,7 +17,8 @@ export default {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
// 使用 reLaunch 清空页面栈,避免通过标题栏/导航栏返回到会员中心等历史页面
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL, {}, 'reLaunch');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -51,7 +52,8 @@ export default {
|
||||
}
|
||||
} else if (res.code == -1) {
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
// 使用 reLaunch 清空页面栈,避免通过标题栏/导航栏返回到注销流程的历史页面
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL, {}, 'reLaunch');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
* 场景 K (静态): member-public.js / cancelsuccess.vue 源码里真的用了 'reLaunch'
|
||||
* 场景 L (回归): clearLoginState 必须早于 redirectTo, 不允许反过来
|
||||
*
|
||||
* 退出登录 (logout) 流程:
|
||||
* 场景 M: 退出登录确认 → clearLoginState + reLaunch 到登录页
|
||||
* 场景 N: 退出登录 modal 取消 → 不清登录态, 不跳转
|
||||
* 场景 O: 退出登录跳登录页必须用 reLaunch 模式 (静态)
|
||||
* 场景 P: getCancelStatus 兜底 (res.code == -1) → clearLoginState + reLaunch
|
||||
* 场景 Q (回归): logout 调用顺序 — clearLoginState 必须在 redirectTo 之前
|
||||
*
|
||||
* 跑法:
|
||||
*
|
||||
* node --test tests/member-cancel-flow.test.js
|
||||
@@ -525,4 +532,165 @@ test('场景 L (回归): submitCancelApply 的源码顺序 — clearLoginState
|
||||
// 第一个 clearLoginState 必须早于第一个 redirectTo
|
||||
assert.ok(idxClear[0] < idxRedirect[0],
|
||||
'第一个 clearLoginState(' + idxClear[0] + ') 必须早于第一个 redirectTo(' + idxRedirect[0] + ')');
|
||||
});
|
||||
|
||||
// =============================================================
|
||||
// 退出登录 (logout) — 同样的"页面栈没清"问题修复
|
||||
//
|
||||
// 修复前 bug: 用户在会员中心退出登录 → redirectTo 默认是 navigateTo 模式 →
|
||||
// 跳到登录页 → 按标题栏/导航栏返回 → 回到会员中心看到仍渲染的旧会员信息。
|
||||
//
|
||||
// 修复: logout / getCancelStatus 兜底分支都用 reLaunch 模式
|
||||
// =============================================================
|
||||
|
||||
// 场景 M: 退出登录确认 → clearLoginState + reLaunch 到登录页
|
||||
test('场景 M: 退出登录确认 → clearLoginState + reLaunch 到登录页 (清栈防回退)', () => {
|
||||
const { mockThis, calls } = createThisMock();
|
||||
bindMethods(mockThis, loadMemberPublic({ uni: mockThis.uni }));
|
||||
|
||||
mockThis.logout();
|
||||
|
||||
// ★ 关键修复: 跳登录页必须用 reLaunch (清空页面栈)
|
||||
assert.equal(calls.redirectTo.length, 1);
|
||||
assert.equal(calls.redirectTo[0].mode, 'reLaunch',
|
||||
'退出登录跳转模式必须是 reLaunch (否则按返回键会回到会员中心), 实际: ' + calls.redirectTo[0].mode);
|
||||
assert.equal(calls.redirectTo[0].to, '/pages_tool/login/login',
|
||||
'退出登录应跳登录页');
|
||||
|
||||
// 登录态必须清
|
||||
assert.ok(calls.commit.some(([t]) => t === 'setToken'),
|
||||
'退出登录必须清 token');
|
||||
assert.ok(calls.commit.some(([t]) => t === 'setMemberInfo'),
|
||||
'退出登录必须清 setMemberInfo');
|
||||
assert.equal(calls.commit.find(([t]) => t === 'setToken')[1], '',
|
||||
'setToken 应清成空串');
|
||||
|
||||
// emptyCart 必须派发
|
||||
assert.ok(calls.dispatch.some(([t]) => t === 'emptyCart'),
|
||||
'退出登录必须派发 emptyCart');
|
||||
|
||||
// localStorage 必须清
|
||||
assert.ok(calls.removeStorage.includes('authInfo'),
|
||||
'退出登录必须清 authInfo localStorage');
|
||||
assert.ok(calls.removeStorage.includes('privacyAgreed'),
|
||||
'退出登录必须清 privacyAgreed localStorage');
|
||||
|
||||
// ★ 关键: clearLoginState 必须在 redirectTo 之前调用 (否则页面栈先 push 登录页)
|
||||
// 找到 clearLoginState 引发的首个 commit (setToken)
|
||||
const tokenIdx = calls.commit.findIndex(([t]) => t === 'setToken');
|
||||
assert.ok(tokenIdx >= 0, '应有 setToken commit');
|
||||
// commit 是同步的, redirectTo 在 commit 之后 (因为 success 回调里先调 clearLoginState 再调 redirectTo)
|
||||
// 但 redirectTo 的调用顺序晚于所有 commit, 这里检查 calls 的总顺序
|
||||
// 通过检查 calls.commit 中最后一个 commit 是否为 setMemberInfo 来佐证
|
||||
assert.equal(calls.commit[calls.commit.length - 1][0], 'setMemberInfo',
|
||||
'setMemberInfo 应是最后一次 commit (clearLoginState 内最后一步)');
|
||||
});
|
||||
|
||||
// 场景 N: 退出登录 modal 取消 → 不清登录态, 不跳转
|
||||
test('场景 N: 退出登录 modal 点取消 → 不清登录态, 不跳转', () => {
|
||||
const { mockThis, calls } = createThisMock({ showModalChoice: 'cancel' });
|
||||
bindMethods(mockThis, loadMemberPublic({ uni: mockThis.uni }));
|
||||
|
||||
mockThis.logout();
|
||||
|
||||
// 不应有任何 commit / dispatch / redirectTo / removeStorage
|
||||
assert.equal(calls.commit.length, 0, '点取消不应清登录态');
|
||||
assert.equal(calls.dispatch.length, 0, '点取消不应派发 emptyCart');
|
||||
assert.equal(calls.redirectTo.length, 0, '点取消不应跳转');
|
||||
assert.equal(calls.removeStorage.length, 0, '点取消不应清 localStorage');
|
||||
});
|
||||
|
||||
// 场景 O (静态): logout 跳登录页的 redirectTo 必须用 reLaunch
|
||||
test('场景 O (静态): member-public.js logout 内跳登录页的 redirectTo 必须用 reLaunch 模式', () => {
|
||||
const src = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'pages_tool', 'member', 'public', 'js', 'member-public.js'),
|
||||
'utf8',
|
||||
);
|
||||
// 找到 logout 函数体
|
||||
const m = src.match(/logout\s*\(\s*\)\s*\{([\s\S]*?)\n\s{3}\}/);
|
||||
assert.ok(m, '应能找到 logout 函数定义');
|
||||
const body = m[1];
|
||||
|
||||
// 提取所有 this.$util.redirectTo( 调用
|
||||
const redirectCalls = body.match(/this\.\$util\.redirectTo\s*\([^)]+\)/g) || [];
|
||||
assert.ok(redirectCalls.length >= 1,
|
||||
'logout 应至少有 1 处 redirectTo (到登录页), 实际 ' + redirectCalls.length);
|
||||
|
||||
for (const call of redirectCalls) {
|
||||
assert.match(call, /['"]reLaunch['"]/,
|
||||
'logout 内的 redirectTo 必须用 reLaunch 模式 (防回退看到旧会员信息), 实际: ' + call.trim());
|
||||
}
|
||||
|
||||
// 跳的必须是登录页
|
||||
assert.ok(redirectCalls[0].includes('LOGIN_PAGE_URL'),
|
||||
'logout 跳的目标必须是 LOGIN_PAGE_URL, 实际: ' + redirectCalls[0].trim());
|
||||
});
|
||||
|
||||
// 场景 P: getCancelStatus 兜底分支 (res.code == -1) → clearLoginState + reLaunch
|
||||
test('场景 P: getCancelStatus res.code == -1 → clearLoginState + reLaunch 到登录页', () => {
|
||||
const { mockThis, calls } = createThisMock();
|
||||
bindMethods(mockThis, loadMemberPublic({ uni: mockThis.uni }));
|
||||
|
||||
// 模拟后端返回 -1 (token 失效)
|
||||
mockThis.$api.sendRequest = (opts) => {
|
||||
calls.api.push({ url: opts.url });
|
||||
if (typeof opts.success === 'function') {
|
||||
opts.success({ code: -1, message: 'token invalid', data: null });
|
||||
}
|
||||
};
|
||||
|
||||
mockThis.getCancelStatus();
|
||||
|
||||
// 必须清登录态
|
||||
assert.ok(calls.commit.some(([t]) => t === 'setToken'),
|
||||
'getCancelStatus 兜底分支必须清 token');
|
||||
// 必须 reLaunch (不能用 navigateTo, 否则页面栈残留)
|
||||
assert.equal(calls.redirectTo.length, 1);
|
||||
assert.equal(calls.redirectTo[0].mode, 'reLaunch',
|
||||
'getCancelStatus 兜底分支跳转必须是 reLaunch, 实际: ' + calls.redirectTo[0].mode);
|
||||
assert.equal(calls.redirectTo[0].to, '/pages_tool/login/login',
|
||||
'getCancelStatus 兜底分支应跳登录页');
|
||||
});
|
||||
|
||||
// 场景 Q (回归): logout 调用顺序 — clearLoginState 必须在 redirectTo 之前
|
||||
test('场景 Q (回归): member-public.js logout 内 clearLoginState 必须早于 redirectTo', () => {
|
||||
const src = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'pages_tool', 'member', 'public', 'js', 'member-public.js'),
|
||||
'utf8',
|
||||
);
|
||||
const m = src.match(/logout\s*\(\s*\)\s*\{([\s\S]*?)\n\s{3}\}/);
|
||||
assert.ok(m, '应能找到 logout 函数定义');
|
||||
const body = m[1];
|
||||
|
||||
const clearIdx = body.indexOf('this.clearLoginState()');
|
||||
const redirectIdx = body.indexOf('this.$util.redirectTo(');
|
||||
|
||||
assert.ok(clearIdx > -1, 'logout 必须调用 clearLoginState()');
|
||||
assert.ok(redirectIdx > -1, 'logout 必须调用 redirectTo()');
|
||||
assert.ok(clearIdx < redirectIdx,
|
||||
'clearLoginState 必须在 redirectTo 之前调用, 实际位置: clear=' + clearIdx + ', redirect=' + redirectIdx);
|
||||
});
|
||||
|
||||
// 场景 R (回归): getCancelStatus 兜底分支 也满足顺序
|
||||
test('场景 R (回归): member-public.js getCancelStatus res.code == -1 分支: clearLoginState 必须早于 redirectTo', () => {
|
||||
const src = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'pages_tool', 'member', 'public', 'js', 'member-public.js'),
|
||||
'utf8',
|
||||
);
|
||||
// 找到 getCancelStatus 内 res.code == -1 分支
|
||||
const funcMatch = src.match(/getCancelStatus\s*\(\s*\)\s*\{([\s\S]*?)\n\s{3}\}/);
|
||||
assert.ok(funcMatch, '应能找到 getCancelStatus 函数定义');
|
||||
const body = funcMatch[1];
|
||||
|
||||
// 找 res.code == -1 那个 else if 分支体
|
||||
const branchMatch = body.match(/res\.code\s*==\s*-1[\s\S]*?(?=\n\s{3}\}|\n\s{4}\})/);
|
||||
assert.ok(branchMatch, '应能找到 res.code == -1 分支');
|
||||
const branchBody = branchMatch[0];
|
||||
|
||||
const clearIdx = branchBody.indexOf('this.clearLoginState()');
|
||||
const redirectIdx = branchBody.indexOf('this.$util.redirectTo(');
|
||||
assert.ok(clearIdx > -1, 'res.code == -1 分支必须调用 clearLoginState()');
|
||||
assert.ok(redirectIdx > -1, 'res.code == -1 分支必须调用 redirectTo()');
|
||||
assert.ok(clearIdx < redirectIdx,
|
||||
'res.code == -1 分支: clearLoginState 必须在 redirectTo 之前, 实际: clear=' + clearIdx + ', redirect=' + redirectIdx);
|
||||
});
|
||||
Reference in New Issue
Block a user