修复:保障移动端批次模式可达

This commit is contained in:
焦龙言
2026-09-23 16:24:03 +08:00
parent 9ca95125b8
commit a7dc1a2a1e
2 changed files with 139 additions and 8 deletions
@@ -1,11 +1,14 @@
import assert from 'node:assert/strict'
import { execFile } from 'node:child_process'
import { lstat, readFile, realpath } from 'node:fs/promises'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { promisify } from 'node:util'
import * as testBatchContext from '../src/utils/testBatchContext.js'
const frontendRoot = fileURLToPath(new URL('..', import.meta.url))
const execFileAsync = promisify(execFile)
const expectedRequests = {
'src/api/master/strain.js': [
@@ -837,6 +840,108 @@ function hasExpectedRequest(expectedRequest) {
const source = (relativePath) => readFile(path.join(frontendRoot, relativePath), 'utf8')
async function runRequestUnavailableInterceptorContract() {
const harness = String.raw`
const { readFile } = require('node:fs/promises')
const vm = require('node:vm')
async function main() {
const requestPath = process.argv[1]
const requestSource = await readFile(requestPath, 'utf8')
const interceptors = {}
const recoveries = []
const service = {
interceptors: {
request: { use: (fulfilled, rejected) => { interceptors.request = { fulfilled, rejected } } },
response: { use: (fulfilled, rejected) => { interceptors.response = { fulfilled, rejected } } }
},
post: () => Promise.resolve()
}
const context = vm.createContext({ console, Promise, Error, JSON, Object, Date, setTimeout })
const moduleFromExports = exports => new vm.SyntheticModule(
Object.keys(exports),
function setExports() {
for (const [name, value] of Object.entries(exports)) this.setExport(name, value)
},
{ context }
)
const modules = {
axios: { default: { defaults: { headers: {} }, create: () => service } },
'element-plus': {
ElNotification: { error: () => {} },
ElMessageBox: { confirm: () => Promise.resolve() },
ElMessage: () => {},
ElLoading: { service: () => ({ close: () => {} }) }
},
'@/utils/auth': { getToken: () => 'token' },
'@/utils/errorCode': { default: {} },
'@/utils/shierfeng': { tansParams: () => '', blobValidate: () => false },
'@/plugins/cache': { default: { session: { getJSON: () => undefined, setJSON: () => {} } } },
'file-saver': { saveAs: () => {} },
'@/store/modules/user': { default: () => ({ logOut: () => Promise.resolve() }) },
'@/utils/testBatchContext': {
applyTestBatchHeader: config => config,
getStoredTestBatchId: () => '',
recoverUnavailableTestBatch: async ({ notify, message }) => {
recoveries.push(message)
notify(message)
}
},
'@/utils/transportCrypto': {
decryptTransportErrorResponse: async error => error,
decryptTransportResponse: async response => response,
encryptTransportRequest: async config => config,
invalidateTransportKeyMeta: () => {},
resetTransportRequestConfig: () => {},
shouldRetryTransportWithFreshKey: () => false
}
}
const requestModule = new vm.SourceTextModule(requestSource, {
context,
identifier: requestPath,
initializeImportMeta(meta) {
meta.env = { VITE_APP_BASE_API: '' }
}
})
await requestModule.link(specifier => {
if (!modules[specifier]) throw new Error('未受控依赖: ' + specifier)
return moduleFromExports(modules[specifier])
})
await requestModule.evaluate()
const invoke = async callback => {
try {
await callback()
return { resolved: true }
} catch (error) {
return { resolved: false, message: error.message, reason: error.reason }
}
}
const success = await invoke(() => interceptors.response.fulfilled({
data: { reason: 'TEST_BATCH_UNAVAILABLE', msg: '成功响应批次不可用' },
request: { responseType: '' }
}))
const failure = await invoke(() => interceptors.response.rejected({
response: { data: { reason: 'TEST_BATCH_UNAVAILABLE', msg: '异常响应批次不可用' } },
message: 'Request failed with status code 400'
}))
process.stdout.write(JSON.stringify({ recoveries, success, failure }))
}
main().catch(error => {
console.error(error)
process.exitCode = 1
})
`
const { stdout } = await execFileAsync(process.execPath, [
'--experimental-vm-modules',
'-e',
harness,
path.join(frontendRoot, 'src/utils/request.js')
])
return JSON.parse(stdout)
}
test('Web 测试模式统一附加请求头并通过路由重建清空页面状态', async () => {
const appMainSource = await source('src/layout/components/AppMain.vue')
const navbarSource = await source('src/layout/components/Navbar.vue')
@@ -928,6 +1033,22 @@ test('刷新失效批次复用统一回退提示,登出无论远端结果都
assert.equal(testBatchContext.getStoredTestBatchId(), '')
})
test('request.js 的成功与异常响应拦截器都会恢复不可用批次并拒绝原请求', async () => {
const result = await runRequestUnavailableInterceptorContract()
assert.deepEqual(result.recoveries, ['成功响应批次不可用', '异常响应批次不可用'])
assert.deepEqual(result.success, {
resolved: false,
message: '成功响应批次不可用',
reason: 'TEST_BATCH_UNAVAILABLE'
})
assert.deepEqual(result.failure, {
resolved: false,
message: '异常响应批次不可用',
reason: 'TEST_BATCH_UNAVAILABLE'
})
})
test('窄屏模式控件仍保留正式退出与测试批次切换入口', async () => {
const switchSource = await source('src/components/TestBatchModeSwitch/index.vue')
const navbarSource = await source('src/layout/components/Navbar.vue')
@@ -940,6 +1061,8 @@ test('窄屏模式控件仍保留正式退出与测试批次切换入口', async
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/)
assert.match(navbarMobileSection, /\.breadcrumb-container\s*\{\s*display:\s*none/)
assert.match(navbarMobileSection, /\.right-menu\s*\{\s*overflow:\s*visible/)
})
test('17 个页面的真实权限门禁逐项匹配独立硬编码契约', async () => {
@@ -293,7 +293,14 @@ async function toggleTheme(event) {
}
@media screen and (max-width: 767px) {
.navbar .right-menu {
.navbar {
.breadcrumb-container {
display: none;
}
.right-menu {
overflow: visible;
.test-batch-mode {
padding: 0;
}
@@ -306,5 +313,6 @@ async function toggleTheme(event) {
margin-right: 0;
}
}
}
}
</style>