修复:完善测试数据模式回退
This commit is contained in:
@@ -3,6 +3,7 @@ import { lstat, readFile, realpath } from 'node:fs/promises'
|
|||||||
import { test } from 'node:test'
|
import { test } from 'node:test'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
import * as testBatchContext from '../src/utils/testBatchContext.js'
|
||||||
|
|
||||||
const frontendRoot = fileURLToPath(new URL('..', import.meta.url))
|
const frontendRoot = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
|
||||||
@@ -837,30 +838,108 @@ function hasExpectedRequest(expectedRequest) {
|
|||||||
const source = (relativePath) => readFile(path.join(frontendRoot, relativePath), 'utf8')
|
const source = (relativePath) => readFile(path.join(frontendRoot, relativePath), 'utf8')
|
||||||
|
|
||||||
test('Web 测试模式统一附加请求头并通过路由重建清空页面状态', async () => {
|
test('Web 测试模式统一附加请求头并通过路由重建清空页面状态', async () => {
|
||||||
const requestSource = await source('src/utils/request.js')
|
|
||||||
const appMainSource = await source('src/layout/components/AppMain.vue')
|
const appMainSource = await source('src/layout/components/AppMain.vue')
|
||||||
const navbarSource = await source('src/layout/components/Navbar.vue')
|
const navbarSource = await source('src/layout/components/Navbar.vue')
|
||||||
|
|
||||||
assert.match(requestSource, /X-Test-Batch-Id/)
|
|
||||||
assert.match(requestSource, /TEST_BATCH_UNAVAILABLE/)
|
|
||||||
assert.match(appMainSource, /scopeVersion/)
|
assert.match(appMainSource, /scopeVersion/)
|
||||||
assert.match(navbarSource, /TestBatchModeSwitch/)
|
assert.match(navbarSource, /TestBatchModeSwitch/)
|
||||||
assert.match(navbarSource, /测试数据/)
|
assert.match(navbarSource, /测试数据/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Web 测试批次上下文仅持久化 ID,登出清除 ID 且管理员标记严格来自后端布尔值', async () => {
|
test('Web 测试批次上下文仅持久化 ID,登出清除 ID 且管理员标记严格来自后端布尔值', async () => {
|
||||||
const contextSource = await source('src/utils/testBatchContext.js')
|
|
||||||
const storeSource = await source('src/store/modules/testBatch.js')
|
const storeSource = await source('src/store/modules/testBatch.js')
|
||||||
const userSource = await source('src/store/modules/user.js')
|
const userSource = await source('src/store/modules/user.js')
|
||||||
|
|
||||||
assert.match(contextSource, /sessionStorage/)
|
|
||||||
assert.doesNotMatch(contextSource, /batchName|status|admin|permission/i)
|
|
||||||
assert.match(storeSource, /getStoredTestBatchId/)
|
assert.match(storeSource, /getStoredTestBatchId/)
|
||||||
assert.match(storeSource, /setStoredTestBatchId/)
|
assert.match(storeSource, /setStoredTestBatchId/)
|
||||||
assert.match(storeSource, /clearStoredTestBatchId/)
|
assert.match(storeSource, /clearStoredTestBatchId/)
|
||||||
assert.match(userSource, /admin:\s*false/)
|
assert.match(userSource, /admin:\s*false/)
|
||||||
assert.match(userSource, /this\.admin\s*=\s*user\.admin\s*===\s*true/)
|
assert.match(userSource, /this\.admin\s*=\s*user\.admin\s*===\s*true/)
|
||||||
assert.match(userSource, /clearStoredTestBatchId\(\)/)
|
assert.match(userSource, /logoutWithLocalCleanup/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Web 测试批次真实上下文逻辑只在登录测试模式附加请求头,并对并发失效做一次回退', async () => {
|
||||||
|
const storage = new Map()
|
||||||
|
globalThis.sessionStorage = {
|
||||||
|
getItem: key => storage.get(key) || null,
|
||||||
|
setItem: (key, value) => storage.set(key, value),
|
||||||
|
removeItem: key => storage.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formalConfig = { headers: {} }
|
||||||
|
testBatchContext.applyTestBatchHeader(formalConfig, '', 'batch-1')
|
||||||
|
assert.deepEqual(formalConfig.headers, {})
|
||||||
|
|
||||||
|
const testConfig = { headers: {} }
|
||||||
|
testBatchContext.applyTestBatchHeader(testConfig, 'token', 'batch-1')
|
||||||
|
assert.equal(testConfig.headers['X-Test-Batch-Id'], 'batch-1')
|
||||||
|
|
||||||
|
testBatchContext.setStoredTestBatchId('batch-1')
|
||||||
|
let loadCount = 0
|
||||||
|
let leaveCount = 0
|
||||||
|
let notifyCount = 0
|
||||||
|
const loadStore = async () => {
|
||||||
|
loadCount += 1
|
||||||
|
await Promise.resolve()
|
||||||
|
return { leaveTestMode: () => { leaveCount += 1 } }
|
||||||
|
}
|
||||||
|
const recovery = {
|
||||||
|
loadStore,
|
||||||
|
notify: () => { notifyCount += 1 },
|
||||||
|
message: '测试批次不可用'
|
||||||
|
}
|
||||||
|
await Promise.all([
|
||||||
|
testBatchContext.recoverUnavailableTestBatch(recovery),
|
||||||
|
testBatchContext.recoverUnavailableTestBatch(recovery),
|
||||||
|
testBatchContext.recoverUnavailableTestBatch(recovery)
|
||||||
|
])
|
||||||
|
assert.equal(loadCount, 1)
|
||||||
|
assert.equal(leaveCount, 1)
|
||||||
|
assert.equal(notifyCount, 1)
|
||||||
|
assert.equal(testBatchContext.getStoredTestBatchId(), '')
|
||||||
|
|
||||||
|
await testBatchContext.recoverUnavailableTestBatch(recovery)
|
||||||
|
assert.equal(loadCount, 2)
|
||||||
|
assert.equal(leaveCount, 2)
|
||||||
|
assert.equal(notifyCount, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('刷新失效批次复用统一回退提示,登出无论远端结果都清理管理员与批次上下文', async () => {
|
||||||
|
const storeSource = await source('src/store/modules/testBatch.js')
|
||||||
|
const userSource = await source('src/store/modules/user.js')
|
||||||
|
assert.match(storeSource, /recoverUnavailableTestBatch/)
|
||||||
|
assert.match(storeSource, /当前测试批次已不可用/)
|
||||||
|
assert.match(userSource, /logoutWithLocalCleanup/)
|
||||||
|
|
||||||
|
globalThis.sessionStorage.setItem('test-batch-id', 'batch-2')
|
||||||
|
const state = { admin: true }
|
||||||
|
await testBatchContext.logoutWithLocalCleanup(() => Promise.resolve('ok'), state)
|
||||||
|
assert.equal(state.admin, false)
|
||||||
|
assert.equal(testBatchContext.getStoredTestBatchId(), '')
|
||||||
|
|
||||||
|
globalThis.sessionStorage.setItem('test-batch-id', 'batch-3')
|
||||||
|
state.admin = true
|
||||||
|
const remoteError = new Error('远端登出失败')
|
||||||
|
await assert.rejects(
|
||||||
|
testBatchContext.logoutWithLocalCleanup(() => Promise.reject(remoteError), state),
|
||||||
|
remoteError
|
||||||
|
)
|
||||||
|
assert.equal(state.admin, false)
|
||||||
|
assert.equal(testBatchContext.getStoredTestBatchId(), '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('窄屏模式控件仍保留正式退出与测试批次切换入口', async () => {
|
||||||
|
const switchSource = await source('src/components/TestBatchModeSwitch/index.vue')
|
||||||
|
const navbarSource = await source('src/layout/components/Navbar.vue')
|
||||||
|
const mobileSection = switchSource.slice(switchSource.indexOf('@media screen and (max-width: 767px)'))
|
||||||
|
const navbarMobileSection = navbarSource.slice(navbarSource.indexOf('@media screen and (max-width: 767px)'))
|
||||||
|
assert.match(mobileSection, /\.mode-selector[\s\S]*\.batch-selector/)
|
||||||
|
assert.doesNotMatch(mobileSection, /display:\s*none/)
|
||||||
|
assert.match(mobileSection, /max-width:\s*48px/)
|
||||||
|
assert.match(mobileSection, /width:\s*72px/)
|
||||||
|
assert.match(mobileSection, /padding-right:\s*2px/)
|
||||||
|
assert.match(mobileSection, /padding-left:\s*2px/)
|
||||||
|
assert.match(navbarMobileSection, /\.test-batch-mode\s*\{\s*padding:\s*0/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('17 个页面的真实权限门禁逐项匹配独立硬编码契约', async () => {
|
test('17 个页面的真实权限门禁逐项匹配独立硬编码契约', async () => {
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ onMounted(async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
testBatchState.options = []
|
testBatchState.options = []
|
||||||
hasLoadError.value = true
|
hasLoadError.value = true
|
||||||
|
if (error?.reason !== 'TEST_BATCH_UNAVAILABLE') {
|
||||||
ElMessage.error('测试批次选项加载失败')
|
ElMessage.error('测试批次选项加载失败')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -173,13 +175,22 @@ async function handleBatchSelect(batchId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.mode-selector,
|
.mode-tag {
|
||||||
.batch-selector {
|
max-width: 48px;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-tag {
|
.mode-selector {
|
||||||
max-width: 116px;
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-selector {
|
||||||
|
width: 72px;
|
||||||
|
flex-basis: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.mode-selector .el-radio-button__inner) {
|
||||||
|
padding-right: 2px;
|
||||||
|
padding-left: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -294,6 +294,10 @@ async function toggleTheme(event) {
|
|||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.navbar .right-menu {
|
.navbar .right-menu {
|
||||||
|
.test-batch-mode {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-container .avatar-wrapper .user-nickname {
|
.avatar-container .avatar-wrapper .user-nickname {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import { listAvailableTestBatches } from '@/api/system/testBatch'
|
import { listAvailableTestBatches } from '@/api/system/testBatch'
|
||||||
import {
|
import {
|
||||||
clearStoredTestBatchId,
|
clearStoredTestBatchId,
|
||||||
getStoredTestBatchId,
|
getStoredTestBatchId,
|
||||||
|
recoverUnavailableTestBatch,
|
||||||
setStoredTestBatchId
|
setStoredTestBatchId
|
||||||
} from '@/utils/testBatchContext'
|
} from '@/utils/testBatchContext'
|
||||||
|
|
||||||
@@ -22,7 +24,11 @@ const useTestBatchStore = defineStore('testBatch', {
|
|||||||
this.options = Array.isArray(response.data) ? response.data : []
|
this.options = Array.isArray(response.data) ? response.data : []
|
||||||
const current = this.options.find(item => String(item.batchId) === String(this.currentBatchId))
|
const current = this.options.find(item => String(item.batchId) === String(this.currentBatchId))
|
||||||
if (this.currentBatchId && (!current || current.status !== 'ACTIVE')) {
|
if (this.currentBatchId && (!current || current.status !== 'ACTIVE')) {
|
||||||
this.leaveTestMode()
|
await recoverUnavailableTestBatch({
|
||||||
|
loadStore: async () => this,
|
||||||
|
notify: message => ElMessage({ message, type: 'warning', duration: 5 * 1000 }),
|
||||||
|
message: '当前测试批次已不可用,已切回正式数据模式'
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.currentBatchName = current?.batchName || ''
|
this.currentBatchName = current?.batchName || ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { login, logout, getInfo } from '@/api/login'
|
|||||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
import { getToken, setToken, removeToken } from '@/utils/auth'
|
||||||
import { isHttp, isEmpty } from "@/utils/validate"
|
import { isHttp, isEmpty } from "@/utils/validate"
|
||||||
import defAva from '@/assets/images/profile.jpg'
|
import defAva from '@/assets/images/profile.jpg'
|
||||||
import { clearStoredTestBatchId } from '@/utils/testBatchContext'
|
import { logoutWithLocalCleanup } from '@/utils/testBatchContext'
|
||||||
|
|
||||||
const useUserStore = defineStore(
|
const useUserStore = defineStore(
|
||||||
'user',
|
'user',
|
||||||
@@ -76,18 +76,11 @@ const useUserStore = defineStore(
|
|||||||
},
|
},
|
||||||
// 退出系统
|
// 退出系统
|
||||||
logOut() {
|
logOut() {
|
||||||
return new Promise((resolve, reject) => {
|
return logoutWithLocalCleanup(() => logout(this.token), this).finally(() => {
|
||||||
logout(this.token).then(() => {
|
|
||||||
this.token = ''
|
this.token = ''
|
||||||
this.roles = []
|
this.roles = []
|
||||||
this.permissions = []
|
this.permissions = []
|
||||||
this.admin = false
|
|
||||||
removeToken()
|
removeToken()
|
||||||
clearStoredTestBatchId()
|
|
||||||
resolve()
|
|
||||||
}).catch(error => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { tansParams, blobValidate } from '@/utils/shierfeng'
|
|||||||
import cache from '@/plugins/cache'
|
import cache from '@/plugins/cache'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import useUserStore from '@/store/modules/user'
|
import useUserStore from '@/store/modules/user'
|
||||||
import { clearStoredTestBatchId, getStoredTestBatchId } from '@/utils/testBatchContext'
|
import {
|
||||||
|
applyTestBatchHeader,
|
||||||
|
getStoredTestBatchId,
|
||||||
|
recoverUnavailableTestBatch
|
||||||
|
} from '@/utils/testBatchContext'
|
||||||
import {
|
import {
|
||||||
decryptTransportErrorResponse,
|
decryptTransportErrorResponse,
|
||||||
decryptTransportResponse,
|
decryptTransportResponse,
|
||||||
@@ -22,11 +26,17 @@ export let isRelogin = { show: false };
|
|||||||
|
|
||||||
async function rejectUnavailableTestBatch(data) {
|
async function rejectUnavailableTestBatch(data) {
|
||||||
const message = data.msg || '当前测试批次不可用,已切回正式数据模式'
|
const message = data.msg || '当前测试批次不可用,已切回正式数据模式'
|
||||||
clearStoredTestBatchId()
|
await recoverUnavailableTestBatch({
|
||||||
|
loadStore: async () => {
|
||||||
const { default: useTestBatchStore } = await import('@/store/modules/testBatch')
|
const { default: useTestBatchStore } = await import('@/store/modules/testBatch')
|
||||||
useTestBatchStore().leaveTestMode()
|
return useTestBatchStore()
|
||||||
ElMessage({ message, type: 'error', duration: 5 * 1000 })
|
},
|
||||||
return Promise.reject(new Error(message))
|
notify: recoveryMessage => ElMessage({ message: recoveryMessage, type: 'error', duration: 5 * 1000 }),
|
||||||
|
message
|
||||||
|
})
|
||||||
|
const error = new Error(message)
|
||||||
|
error.reason = data.reason
|
||||||
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTestBatchUnavailable(data) {
|
function isTestBatchUnavailable(data) {
|
||||||
@@ -59,10 +69,7 @@ service.interceptors.request.use(async config => {
|
|||||||
if (getToken() && !isToken) {
|
if (getToken() && !isToken) {
|
||||||
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||||
}
|
}
|
||||||
const testBatchId = getStoredTestBatchId()
|
applyTestBatchHeader(config, getToken(), getStoredTestBatchId())
|
||||||
if (getToken() && testBatchId) {
|
|
||||||
config.headers['X-Test-Batch-Id'] = String(testBatchId)
|
|
||||||
}
|
|
||||||
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
|
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
|
||||||
const requestObj = {
|
const requestObj = {
|
||||||
url: config.url,
|
url: config.url,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const TEST_BATCH_STORAGE_KEY = 'test-batch-id'
|
const TEST_BATCH_STORAGE_KEY = 'test-batch-id'
|
||||||
|
let unavailableBatchRecoveryPromise
|
||||||
|
|
||||||
export function getStoredTestBatchId() {
|
export function getStoredTestBatchId() {
|
||||||
return sessionStorage.getItem(TEST_BATCH_STORAGE_KEY) || ''
|
return sessionStorage.getItem(TEST_BATCH_STORAGE_KEY) || ''
|
||||||
@@ -11,3 +12,36 @@ export function setStoredTestBatchId(batchId) {
|
|||||||
export function clearStoredTestBatchId() {
|
export function clearStoredTestBatchId() {
|
||||||
sessionStorage.removeItem(TEST_BATCH_STORAGE_KEY)
|
sessionStorage.removeItem(TEST_BATCH_STORAGE_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyTestBatchHeader(config, token, batchId) {
|
||||||
|
if (token && batchId) {
|
||||||
|
config.headers = config.headers || {}
|
||||||
|
config.headers['X-Test-Batch-Id'] = String(batchId)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLocalUserContext(state) {
|
||||||
|
state.admin = false
|
||||||
|
clearStoredTestBatchId()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logoutWithLocalCleanup(remoteLogout, state) {
|
||||||
|
return Promise.resolve().then(remoteLogout).finally(() => {
|
||||||
|
clearLocalUserContext(state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recoverUnavailableTestBatch({ loadStore, notify, message }) {
|
||||||
|
if (!unavailableBatchRecoveryPromise) {
|
||||||
|
unavailableBatchRecoveryPromise = (async () => {
|
||||||
|
clearStoredTestBatchId()
|
||||||
|
const store = await loadStore()
|
||||||
|
store.leaveTestMode()
|
||||||
|
notify(message)
|
||||||
|
})().finally(() => {
|
||||||
|
unavailableBatchRecoveryPromise = undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return unavailableBatchRecoveryPromise
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user