Merge remote-tracking branch 'origin/main'

This commit is contained in:
caozehui
2026-05-18 13:19:45 +08:00
11 changed files with 187 additions and 179 deletions

View File

@@ -208,7 +208,9 @@ export async function fetchRefreshToken(refreshToken: string): Promise<ServiceRe
method: 'post', method: 'post',
params: { refreshToken }, params: { refreshToken },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
skipAuth: true skipAuth: true,
suppressErrorMessage: true,
skipTokenRefresh: true
}); });
if (result.error || !result.data) { if (result.error || !result.data) {

View File

@@ -10,6 +10,10 @@ declare module 'axios' {
* 避免给它们带上过期 access 头被网关拦截。 * 避免给它们带上过期 access 头被网关拦截。
*/ */
skipAuth?: boolean; skipAuth?: boolean;
/** 请求失败时不走通用错误 toast由调用方自行收敛提示。 */
suppressErrorMessage?: boolean;
/** 请求失败命中过期 access code 时,不再触发 refresh-token 流程。 */
skipTokenRefresh?: boolean;
} }
} }

View File

@@ -0,0 +1,32 @@
export const SESSION_EXPIRED_MESSAGE = '登录已失效,请重新登录';
export interface ErrorMessageSuppressOptions {
backendErrorCode: string;
suppressErrorMessage?: boolean;
logoutCodes: string[];
modalLogoutCodes: string[];
expiredTokenCodes: string[];
}
export interface BackendFailDeferOptions {
suppressErrorMessage?: boolean;
skipTokenRefresh?: boolean;
}
export function parseServiceCodes(codes?: string) {
return codes?.split(',').filter(Boolean) || [];
}
export function shouldDeferBackendFailToCaller(options: BackendFailDeferOptions) {
return Boolean(options.suppressErrorMessage && options.skipTokenRefresh);
}
export function shouldSuppressErrorMessage(options: ErrorMessageSuppressOptions) {
if (options.suppressErrorMessage) {
return true;
}
const handledCodes = [...options.logoutCodes, ...options.modalLogoutCodes, ...options.expiredTokenCodes];
return handledCodes.includes(options.backendErrorCode);
}

View File

@@ -5,6 +5,7 @@ import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service'; import { getServiceBaseURL } from '@/utils/service';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { applyApiEncrypt } from './api-encrypt'; import { applyApiEncrypt } from './api-encrypt';
import { parseServiceCodes, shouldDeferBackendFailToCaller, shouldSuppressErrorMessage } from './error-message';
import { getAuthorization, handleExpiredRequest, notifySessionExpired, showErrorMsg } from './shared'; import { getAuthorization, handleExpiredRequest, notifySessionExpired, showErrorMsg } from './shared';
import { withDedupe } from './dedupe'; import { withDedupe } from './dedupe';
import type { RequestInstanceState } from './type'; import type { RequestInstanceState } from './type';
@@ -48,6 +49,15 @@ export const request = withDedupe(
const authStore = useAuthStore(); const authStore = useAuthStore();
const responseCode = String(response.data.code); const responseCode = String(response.data.code);
if (
shouldDeferBackendFailToCaller({
suppressErrorMessage: response.config.suppressErrorMessage,
skipTokenRefresh: response.config.skipTokenRefresh
})
) {
return null;
}
function handleLogout() { function handleLogout() {
authStore.resetStore(); authStore.resetStore();
} }
@@ -61,14 +71,14 @@ export const request = withDedupe(
// 当后端返回码命中 `logoutCodes` 时,表示登录态已失效,需要提示后退出登录 // 当后端返回码命中 `logoutCodes` 时,表示登录态已失效,需要提示后退出登录
// 走 notifySessionExpired 而不是裸 resetStore保证并发请求只弹一次 toast、只清一次状态 // 走 notifySessionExpired 而不是裸 resetStore保证并发请求只弹一次 toast、只清一次状态
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || []; const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
if (logoutCodes.includes(responseCode)) { if (logoutCodes.includes(responseCode)) {
notifySessionExpired(); notifySessionExpired();
return null; return null;
} }
// 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录 // 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || []; const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) { if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg]; request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
@@ -92,8 +102,13 @@ export const request = withDedupe(
// 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token // 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token
// `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes` // `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes`
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []; const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
if (expiredTokenCodes.includes(responseCode)) { if (expiredTokenCodes.includes(responseCode)) {
if (response.config.skipTokenRefresh) {
notifySessionExpired();
return null;
}
const success = await handleExpiredRequest(request.state); const success = await handleExpiredRequest(request.state);
if (success) { if (success) {
const Authorization = getAuthorization(); const Authorization = getAuthorization();
@@ -117,15 +132,19 @@ export const request = withDedupe(
backendErrorCode = String(error.response?.data?.code || ''); backendErrorCode = String(error.response?.data?.code || '');
} }
// 这类错误信息已经通过弹窗展示,不再重复提示 const suppressErrorMessage = Boolean(error.config?.suppressErrorMessage);
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || []; const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
if (modalLogoutCodes.includes(backendErrorCode)) { const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
return; const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
} if (
shouldSuppressErrorMessage({
// token 过期时会自动刷新并重试请求,这里无需额外提示 backendErrorCode,
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []; suppressErrorMessage,
if (expiredTokenCodes.includes(backendErrorCode)) { logoutCodes,
modalLogoutCodes,
expiredTokenCodes
})
) {
return; return;
} }

View File

@@ -1,6 +1,7 @@
import { useAuthStore } from '@/store/modules/auth'; import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
import { fetchRefreshToken } from '../api'; import { fetchRefreshToken } from '../api';
import { SESSION_EXPIRED_MESSAGE } from './error-message';
import type { RequestInstanceState } from './type'; import type { RequestInstanceState } from './type';
export function getAuthorization() { export function getAuthorization() {
@@ -51,7 +52,7 @@ export function notifySessionExpired() {
if (sessionExpiredNotified) return; if (sessionExpiredNotified) return;
sessionExpiredNotified = true; sessionExpiredNotified = true;
window.$message?.error('登录已过期,请重新登录'); window.$message?.error(SESSION_EXPIRED_MESSAGE);
const { resetStore } = useAuthStore(); const { resetStore } = useAuthStore();
resetStore(); resetStore();

View File

@@ -51,16 +51,27 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
clearAuthStorage(); clearAuthStorage();
authStore.$reset(); // setup store 没有内置 $reset需要显式重置内部状态避免 token / userInfo 残留导致 isLogin 误判。
dictStore.resetDictCache(); token.value = '';
objectContextStore.$reset(); Object.assign(userInfo, {
userId: '',
userName: '',
nickname: '',
roles: [],
buttons: []
});
if (!route.meta.constant) { dictStore.resetDictCache();
objectContextStore.clearContext();
// 用路由名判断当前是否已在登录页,避免依赖 route.meta.constant ——
// workbench 等首页也是常量路由,原写法会让常量路由上的登出请求不跳转。
if (route.name !== 'login') {
await toLogin(); await toLogin();
} }
tabStore.cacheTabs(); tabStore.cacheTabs();
routeStore.resetStore(); await routeStore.resetStore();
} }
/** Record the user ID of the previous login session Used to compare with the current user ID on next login */ /** Record the user ID of the previous login session Used to compare with the current user ID on next login */

View File

@@ -149,9 +149,16 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
/** 重置 store */ /** 重置 store */
async function resetStore() { async function resetStore() {
const routeStore = useRouteStore(); // setup store 没有内置 $reset需要显式重置内部状态。
// 否则 isInitConstantRoute / isInitAuthRoute 一直停在 true导致下面 initConstantRoute 早返,
routeStore.$reset(); // 路由被 resetVueRoutes 摘掉后无法重新注册,菜单和导航都会失效。
setIsInitConstantRoute(false);
setIsInitAuthRoute(false);
constantRoutes.value = [];
authRoutes.value = [];
menus.value = [];
cacheRoutes.value = [];
excludeCacheRoutes.value = [];
resetVueRoutes(); resetVueRoutes();

View File

@@ -3,7 +3,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
import type { Component } from 'vue'; import type { Component } from 'vue';
import { ElButton, ElTag } from 'element-plus'; import { ElButton, ElTag } from 'element-plus';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { CircleCheckFilled, DeleteFilled, FolderOpened, VideoPause } from '@element-plus/icons-vue'; import { Box, DeleteFilled, VideoPause, VideoPlay } from '@element-plus/icons-vue';
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict'; import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context'; import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api'; import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
@@ -76,14 +76,14 @@ const statusNavMetas: StatusNavMeta[] = [
label: '启用产品', label: '启用产品',
description: '当前正常服务中的产品', description: '当前正常服务中的产品',
tone: 'teal', tone: 'teal',
icon: CircleCheckFilled icon: VideoPlay
}, },
{ {
key: 'archived', key: 'archived',
label: '归档产品', label: '归档产品',
description: '已完成阶段目标的产品', description: '已完成阶段目标的产品',
tone: 'slate', tone: 'slate',
icon: FolderOpened icon: Box
}, },
{ {
key: 'paused', key: 'paused',
@@ -109,7 +109,7 @@ const operateVisible = ref(false);
const editingRow = ref<Api.Product.Product | null>(null); const editingRow = ref<Api.Product.Product | null>(null);
const { routerPush } = useRouterPush(); const { routerPush } = useRouterPush();
const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE); const { getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
const statusCounts = ref<Record<string, number>>({ const statusCounts = ref<Record<string, number>>({
active: 0, active: 0,
@@ -129,29 +129,6 @@ const statusItems = computed(() =>
})) }))
); );
const overviewMetrics = computed(() => [
{
label: '可见产品',
value: Object.values(statusCounts.value).reduce((sum, count) => sum + count, 0),
hint: '当前接口可查询到的产品总量'
},
{
label: '当前启用',
value: statusCounts.value.active ?? 0,
hint: '正在持续服务和维护的产品'
},
{
label: '产品方向',
value: directionOptions.value.length,
hint: '已加载的方向字典项数量'
},
{
label: '废弃产品',
value: statusCounts.value.abandoned ?? 0,
hint: '已明确停止建设的产品'
}
]);
function getDirectionLabel(directionCode?: string | null) { function getDirectionLabel(directionCode?: string | null) {
return getDirectionDictLabel(directionCode, '--'); return getDirectionDictLabel(directionCode, '--');
} }
@@ -288,7 +265,7 @@ async function handleResetSearch() {
async function handleStatusChange(status: Api.Product.ProductStatusCode) { async function handleStatusChange(status: Api.Product.ProductStatusCode) {
selectedStatus.value = status; selectedStatus.value = status;
await reloadProductTable(1); await Promise.all([loadOverviewData(), reloadProductTable(1)]);
} }
function openCreate() { function openCreate() {
@@ -326,14 +303,6 @@ onMounted(async () => {
> >
<div class="flex-col-stretch gap-16px xl:min-h-0"> <div class="flex-col-stretch gap-16px xl:min-h-0">
<ElCard class="product-overview-card card-wrapper"> <ElCard class="product-overview-card card-wrapper">
<div class="product-overview-card__stats">
<div v-for="item in overviewMetrics" :key="item.label" class="product-overview-card__stat">
<span class="product-overview-card__stat-label">{{ item.label }}</span>
<strong class="product-overview-card__stat-value">{{ item.value }}</strong>
<small class="product-overview-card__stat-hint">{{ item.hint }}</small>
</div>
</div>
<div class="product-status-panel__list"> <div class="product-status-panel__list">
<button <button
v-for="item in statusItems" v-for="item in statusItems"
@@ -441,45 +410,10 @@ onMounted(async () => {
linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 97%)); linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 97%));
} }
.product-overview-card__stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.product-overview-card__stat {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 18px;
background-color: rgb(255 255 255 / 84%);
}
.product-overview-card__stat-label {
color: rgb(100 116 139 / 90%);
font-size: 13px;
}
.product-overview-card__stat-value {
color: rgb(15 23 42 / 94%);
font-size: 24px;
font-weight: 700;
line-height: 1.1;
}
.product-overview-card__stat-hint {
color: rgb(100 116 139 / 90%);
font-size: 12px;
line-height: 1.5;
}
.product-status-panel__list { .product-status-panel__list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
margin-top: 16px;
} }
.product-status-item { .product-status-item {
@@ -594,10 +528,4 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
} }
} }
@media (width <= 640px) {
.product-overview-card__stats {
grid-template-columns: 1fr;
}
}
</style> </style>

View File

@@ -71,7 +71,7 @@ const operateVisible = ref(false);
const editingRow = ref<Api.Project.Project | null>(null); const editingRow = ref<Api.Project.Project | null>(null);
const { routerPush } = useRouterPush(); const { routerPush } = useRouterPush();
const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE); const { getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
const { getLabel: getProjectTypeLabel } = useDict(RDMS_PROJECT_TYPE_DICT_CODE); const { getLabel: getProjectTypeLabel } = useDict(RDMS_PROJECT_TYPE_DICT_CODE);
const statusCounts = ref<Record<string, number>>({ const statusCounts = ref<Record<string, number>>({
@@ -243,7 +243,7 @@ async function handleResetSearch() {
async function handleStatusChange(status: Api.Project.ProjectStatusCode) { async function handleStatusChange(status: Api.Project.ProjectStatusCode) {
selectedStatus.value = status; selectedStatus.value = status;
await reloadProjectTable(1); await Promise.all([loadOverviewData(), reloadProjectTable(1)]);
} }
function openCreate() { function openCreate() {
@@ -282,7 +282,6 @@ onMounted(async () => {
<div class="flex-col-stretch gap-16px xl:min-h-0"> <div class="flex-col-stretch gap-16px xl:min-h-0">
<ProjectOverviewCard <ProjectOverviewCard
:status-counts="statusCounts" :status-counts="statusCounts"
:direction-count="directionOptions.length"
:selected-status="selectedStatus" :selected-status="selectedStatus"
@status-change="handleStatusChange" @status-change="handleStatusChange"
/> />

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import type { Component } from 'vue'; import type { Component } from 'vue';
import { CircleCheckFilled, DeleteFilled, DocumentAdd, FolderOpened, VideoPause } from '@element-plus/icons-vue'; import { Box, CircleCheckFilled, DeleteFilled, DocumentAdd, VideoPause, VideoPlay } from '@element-plus/icons-vue';
defineOptions({ name: 'ProjectOverviewCard' }); defineOptions({ name: 'ProjectOverviewCard' });
@@ -15,7 +15,6 @@ interface StatusNavMeta {
interface Props { interface Props {
statusCounts: Record<string, number>; statusCounts: Record<string, number>;
directionCount: number;
selectedStatus: Api.Project.ProjectStatusCode; selectedStatus: Api.Project.ProjectStatusCode;
} }
@@ -40,7 +39,7 @@ const statusNavMetas: StatusNavMeta[] = [
label: '进行中', label: '进行中',
description: '正在执行的项目', description: '正在执行的项目',
tone: 'teal', tone: 'teal',
icon: CircleCheckFilled icon: VideoPlay
}, },
{ {
key: 'paused', key: 'paused',
@@ -54,7 +53,7 @@ const statusNavMetas: StatusNavMeta[] = [
label: '已完成', label: '已完成',
description: '达成目标的项目', description: '达成目标的项目',
tone: 'teal', tone: 'teal',
icon: FolderOpened icon: CircleCheckFilled
}, },
{ {
key: 'cancelled', key: 'cancelled',
@@ -68,7 +67,7 @@ const statusNavMetas: StatusNavMeta[] = [
label: '归档项目', label: '归档项目',
description: '已收口归档的历史项目', description: '已收口归档的历史项目',
tone: 'slate', tone: 'slate',
icon: FolderOpened icon: Box
} }
]; ];
@@ -79,29 +78,6 @@ const statusItems = computed(() =>
})) }))
); );
const overviewMetrics = computed(() => [
{
label: '总项目数',
value: Object.values(props.statusCounts).reduce((sum, count) => sum + count, 0),
hint: '当前接口可查询到的项目总量'
},
{
label: '进行中',
value: props.statusCounts.active ?? 0,
hint: '正在执行的项目'
},
{
label: '待开始',
value: props.statusCounts.pending ?? 0,
hint: '等待启动的项目'
},
{
label: '作废项目',
value: props.statusCounts.cancelled ?? 0,
hint: '已终止或取消推进的项目'
}
]);
function handleStatusClick(status: Api.Project.ProjectStatusCode) { function handleStatusClick(status: Api.Project.ProjectStatusCode) {
emit('status-change', status); emit('status-change', status);
} }
@@ -109,14 +85,6 @@ function handleStatusClick(status: Api.Project.ProjectStatusCode) {
<template> <template>
<ElCard class="project-overview-card card-wrapper"> <ElCard class="project-overview-card card-wrapper">
<div class="project-overview-card__stats">
<div v-for="item in overviewMetrics" :key="item.label" class="project-overview-card__stat">
<span class="project-overview-card__stat-label">{{ item.label }}</span>
<strong class="project-overview-card__stat-value">{{ item.value }}</strong>
<small class="project-overview-card__stat-hint">{{ item.hint }}</small>
</div>
</div>
<div class="project-status-panel__list"> <div class="project-status-panel__list">
<button <button
v-for="item in statusItems" v-for="item in statusItems"
@@ -153,45 +121,10 @@ function handleStatusClick(status: Api.Project.ProjectStatusCode) {
linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 97%)); linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 97%));
} }
.project-overview-card__stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.project-overview-card__stat {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 18px;
background-color: rgb(255 255 255 / 84%);
}
.project-overview-card__stat-label {
color: rgb(100 116 139 / 90%);
font-size: 13px;
}
.project-overview-card__stat-value {
color: rgb(15 23 42 / 94%);
font-size: 24px;
font-weight: 700;
line-height: 1.1;
}
.project-overview-card__stat-hint {
color: rgb(100 116 139 / 90%);
font-size: 12px;
line-height: 1.5;
}
.project-status-panel__list { .project-status-panel__list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
margin-top: 16px;
} }
.project-status-item { .project-status-item {
@@ -293,10 +226,4 @@ function handleStatusClick(status: Api.Project.ProjectStatusCode) {
flex-direction: column; flex-direction: column;
} }
} }
@media (width <= 640px) {
.project-overview-card__stats {
grid-template-columns: 1fr;
}
}
</style> </style>

View File

@@ -0,0 +1,78 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
SESSION_EXPIRED_MESSAGE,
shouldDeferBackendFailToCaller,
shouldSuppressErrorMessage
} from '../src/service/request/error-message';
describe('request session expired message handling', () => {
it('uses the unified session expired message', () => {
assert.equal(SESSION_EXPIRED_MESSAGE, '登录已失效,请重新登录');
});
it('suppresses raw backend errors when the request is marked silent', () => {
assert.equal(
shouldSuppressErrorMessage({
backendErrorCode: '500',
suppressErrorMessage: true,
logoutCodes: [],
modalLogoutCodes: [],
expiredTokenCodes: []
}),
true
);
});
it('lets refresh-token callers handle every backend failure silently', () => {
assert.equal(
shouldDeferBackendFailToCaller({
suppressErrorMessage: true,
skipTokenRefresh: true
}),
true
);
});
it('suppresses auth lifecycle error codes handled by request flow', () => {
assert.equal(
shouldSuppressErrorMessage({
backendErrorCode: '401',
logoutCodes: ['401'],
modalLogoutCodes: [],
expiredTokenCodes: []
}),
true
);
assert.equal(
shouldSuppressErrorMessage({
backendErrorCode: '7777',
logoutCodes: [],
modalLogoutCodes: ['7777'],
expiredTokenCodes: []
}),
true
);
assert.equal(
shouldSuppressErrorMessage({
backendErrorCode: '1002023000',
logoutCodes: [],
modalLogoutCodes: [],
expiredTokenCodes: ['1002023000']
}),
true
);
});
it('keeps normal backend errors visible', () => {
assert.equal(
shouldSuppressErrorMessage({
backendErrorCode: '500',
logoutCodes: ['401'],
modalLogoutCodes: ['7777'],
expiredTokenCodes: ['1002023000']
}),
false
);
});
});