修复AGC登录与发码系统失败残留忙碌文案
- handleSendCode/handleLoginSubmit 的 catch 在抛出前把系统失败原因写进 loginStatus,离开「正在发送验证码」「正在登录」 - 系统变体按类型化载荷拼出提示文案后再原样抛出,恢复改动前「显示 + 上报」的可见行为 - 本地运行时超时等非载体错误同样先写 loginStatus 再抛出 - appSurface 用例改为断言系统变体文案落到登录页且仍经 unhandledrejection 抛出
This commit is contained in:
@@ -554,7 +554,11 @@ export function AuthenticatedClient({
|
||||
setCodeCooldownSeconds(Math.max(0, Math.floor(response.cooldownSeconds)));
|
||||
setLoginStatus(`验证码已发送,${response.expiresInSeconds} 秒内有效`);
|
||||
} catch (error) {
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) throw error;
|
||||
// 本地运行时超时等我们自己的失败也要离开「正在发送验证码」并给出原因,改动前就是如此。
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) {
|
||||
setLoginStatus(error instanceof Error ? error.message : String(error));
|
||||
throw error;
|
||||
}
|
||||
const failure = error.error;
|
||||
switch (failure.type) {
|
||||
// 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。
|
||||
@@ -644,15 +648,79 @@ export function AuthenticatedClient({
|
||||
setLoginStatus('当前账号没有权限执行此操作');
|
||||
break;
|
||||
}
|
||||
// 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkFailure':
|
||||
case 'authServiceUnavailable':
|
||||
case 'unexpectedRejection':
|
||||
case 'authResponseInvalid':
|
||||
case 'clientSessionPersistFailed':
|
||||
case 'runtimeSessionInstallFailed':
|
||||
case 'authClientInitFailed':
|
||||
// 系统失败:调用方处理不了,先按原始事实给用户可见反馈,再原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkFailure': {
|
||||
const payload = failure as AuthNetworkFailure;
|
||||
setLoginStatus(
|
||||
payload.reason === 'timeout'
|
||||
? '发送验证码失败:登录服务响应超时,请检查服务器地址和网络后重试'
|
||||
: '发送验证码失败:无法连接登录服务,请确认配套后端或 API 代理已启动后重试',
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'authServiceUnavailable': {
|
||||
const payload = failure as AuthServiceUnavailable;
|
||||
setLoginStatus(
|
||||
`发送验证码失败:登录服务暂时不可用(HTTP ${payload.status})`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'unexpectedRejection': {
|
||||
const payload = failure as UnexpectedRejection;
|
||||
setLoginStatus(
|
||||
payload.serverMessage
|
||||
? `发送验证码失败:登录服务请求失败(HTTP ${payload.status}):${payload.serverMessage}`
|
||||
: `发送验证码失败:登录服务请求失败(HTTP ${payload.status})`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'authResponseInvalid': {
|
||||
const payload = failure as AuthResponseInvalid;
|
||||
let text: string;
|
||||
switch (payload.reason) {
|
||||
case 'notJson':
|
||||
text = '发送验证码失败:登录服务响应不是合法 JSON';
|
||||
break;
|
||||
case 'invalidBody':
|
||||
text = '发送验证码失败:登录服务响应结构不符合契约';
|
||||
break;
|
||||
case 'missingRefreshCookie':
|
||||
text = '发送验证码失败:登录服务未返回新的续期凭据';
|
||||
break;
|
||||
case 'missingUserIdentity':
|
||||
text = '发送验证码失败:登录服务未返回用户身份';
|
||||
break;
|
||||
case 'serverRejected':
|
||||
text = payload.serverMessage
|
||||
? `发送验证码失败:登录服务拒绝了本次请求:${payload.serverMessage}`
|
||||
: '发送验证码失败:登录服务拒绝了本次请求';
|
||||
break;
|
||||
default: {
|
||||
expectNever(payload.reason);
|
||||
text = '发送验证码失败:登录服务响应不符合契约';
|
||||
}
|
||||
}
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'clientSessionPersistFailed': {
|
||||
const payload = failure as ClientSessionPersistFailed;
|
||||
setLoginStatus(
|
||||
`发送验证码失败:本机登录凭据读写失败:${payload.detail}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'runtimeSessionInstallFailed': {
|
||||
const payload = failure as RuntimeSessionInstallFailed;
|
||||
setLoginStatus(
|
||||
`发送验证码失败:本机运行时会话安装失败:${payload.detail}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'authClientInitFailed': {
|
||||
setLoginStatus('发送验证码失败:创建登录网络客户端失败');
|
||||
throw error;
|
||||
}
|
||||
default: {
|
||||
expectNever(failure);
|
||||
throw error;
|
||||
@@ -722,7 +790,11 @@ export function AuthenticatedClient({
|
||||
setCode('');
|
||||
setPassword('');
|
||||
} catch (error) {
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) throw error;
|
||||
// 本地运行时超时等我们自己的失败也要离开「正在登录」并给出原因,改动前就是如此。
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) {
|
||||
setLoginStatus(error instanceof Error ? error.message : String(error));
|
||||
throw error;
|
||||
}
|
||||
const failure = error.error;
|
||||
switch (failure.type) {
|
||||
// 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。
|
||||
@@ -812,15 +884,75 @@ export function AuthenticatedClient({
|
||||
setLoginStatus('当前账号没有权限执行此操作');
|
||||
break;
|
||||
}
|
||||
// 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkFailure':
|
||||
case 'authServiceUnavailable':
|
||||
case 'unexpectedRejection':
|
||||
case 'authResponseInvalid':
|
||||
case 'clientSessionPersistFailed':
|
||||
case 'runtimeSessionInstallFailed':
|
||||
case 'authClientInitFailed':
|
||||
// 系统失败:调用方处理不了,先按原始事实给用户可见反馈,再原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkFailure': {
|
||||
const payload = failure as AuthNetworkFailure;
|
||||
setLoginStatus(
|
||||
payload.reason === 'timeout'
|
||||
? '登录失败:登录服务响应超时,请检查服务器地址和网络后重试'
|
||||
: '登录失败:无法连接登录服务,请确认配套后端或 API 代理已启动后重试',
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'authServiceUnavailable': {
|
||||
const payload = failure as AuthServiceUnavailable;
|
||||
setLoginStatus(
|
||||
`登录失败:登录服务暂时不可用(HTTP ${payload.status})`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'unexpectedRejection': {
|
||||
const payload = failure as UnexpectedRejection;
|
||||
setLoginStatus(
|
||||
payload.serverMessage
|
||||
? `登录失败:登录服务请求失败(HTTP ${payload.status}):${payload.serverMessage}`
|
||||
: `登录失败:登录服务请求失败(HTTP ${payload.status})`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
case 'authResponseInvalid': {
|
||||
const payload = failure as AuthResponseInvalid;
|
||||
let text: string;
|
||||
switch (payload.reason) {
|
||||
case 'notJson':
|
||||
text = '登录失败:登录服务响应不是合法 JSON';
|
||||
break;
|
||||
case 'invalidBody':
|
||||
text = '登录失败:登录服务响应结构不符合契约';
|
||||
break;
|
||||
case 'missingRefreshCookie':
|
||||
text = '登录失败:登录服务未返回新的续期凭据';
|
||||
break;
|
||||
case 'missingUserIdentity':
|
||||
text = '登录失败:登录服务未返回用户身份';
|
||||
break;
|
||||
case 'serverRejected':
|
||||
text = payload.serverMessage
|
||||
? `登录失败:登录服务拒绝了本次请求:${payload.serverMessage}`
|
||||
: '登录失败:登录服务拒绝了本次请求';
|
||||
break;
|
||||
default: {
|
||||
expectNever(payload.reason);
|
||||
text = '登录失败:登录服务响应不符合契约';
|
||||
}
|
||||
}
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'clientSessionPersistFailed': {
|
||||
const payload = failure as ClientSessionPersistFailed;
|
||||
setLoginStatus(`登录失败:本机登录凭据读写失败:${payload.detail}`);
|
||||
throw error;
|
||||
}
|
||||
case 'runtimeSessionInstallFailed': {
|
||||
const payload = failure as RuntimeSessionInstallFailed;
|
||||
setLoginStatus(`登录失败:本机运行时会话安装失败:${payload.detail}`);
|
||||
throw error;
|
||||
}
|
||||
case 'authClientInitFailed': {
|
||||
setLoginStatus('登录失败:创建登录网络客户端失败');
|
||||
throw error;
|
||||
}
|
||||
default: {
|
||||
expectNever(failure);
|
||||
throw error;
|
||||
|
||||
@@ -281,7 +281,7 @@ export function registerAuthTests() {
|
||||
expect(screen.queryByLabelText('已登录')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps transport details out of the login copy by throwing system variants through', async () => {
|
||||
it('shows the system variant copy and still throws it through', async () => {
|
||||
const uninstall = installUnhandledRejectionBridge();
|
||||
const reasons: unknown[] = [];
|
||||
const onRejection = (event: Event) => {
|
||||
@@ -301,7 +301,7 @@ export function registerAuthTests() {
|
||||
renderAuthSurface(invoke);
|
||||
await loginWithCode();
|
||||
|
||||
// 先等系统变体真的经 unhandledrejection 抛出,再断言它没落到登录页文案。
|
||||
// 先等系统变体真的经 unhandledrejection 抛出。
|
||||
await waitFor(() => expect(reasons).toHaveLength(1));
|
||||
// 载体自身 message 留空;诊断信息在 error 字段里,由上报链路取。
|
||||
expect(reasons[0]).toBeInstanceOf(ClientAuthErrorWrapper);
|
||||
@@ -309,7 +309,12 @@ export function registerAuthTests() {
|
||||
type: 'authNetworkFailure',
|
||||
reason: 'unreachable',
|
||||
});
|
||||
expect(screen.queryByText(/无法连接登录服务/u)).toBeNull();
|
||||
// 改动前系统失败也会在登录页给出一行提示;这里恢复同样的可见反馈,只是不泄露传输层细节。
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'登录失败:无法连接登录服务,请确认配套后端或 API 代理已启动后重试',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(document.body.textContent).not.toContain('ECONNREFUSED');
|
||||
await screen.findByRole('main', { name: '登录' });
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user