更新登录页及优化相关功能
This commit is contained in:
@@ -30,6 +30,48 @@ test('Web 端统一使用 Sylvan 横版标识、折叠图标和浏览器图标',
|
||||
assert.doesNotMatch(`${fullLogo}\n${markLogo}`, /<script\b|<foreignObject\b|javascript:|\b(?:xlink:)?href=|\bonload=|\bonerror=/i)
|
||||
})
|
||||
|
||||
test('PC 登录页在版权信息旁提供开源协议外链', async () => {
|
||||
const [loginPage, router, permission] = await Promise.all([
|
||||
readFile(path.join(frontendRoot, 'src/views/login.vue'), 'utf8'),
|
||||
readFile(path.join(frontendRoot, 'src/router/index.js'), 'utf8'),
|
||||
readFile(path.join(frontendRoot, 'src/permission.js'), 'utf8')
|
||||
])
|
||||
|
||||
assert.match(loginPage, /开源协议/)
|
||||
assert.match(loginPage, /to="\/open-source-notice"/)
|
||||
assert.doesNotMatch(loginPage, /git\.yujiangrubber\.cn\/niuniu\/ShiErFeng/)
|
||||
assert.match(router, /path: '\/open-source-notice'/)
|
||||
assert.match(router, /@\/views\/openSourceNotice\/index/)
|
||||
assert.match(permission, /whiteList = \['\/login', '\/register', '\/open-source-notice'\]/)
|
||||
})
|
||||
|
||||
test('站内开源协议页展示 RuoYi 来源与 MIT 许可全文', async () => {
|
||||
const noticePage = await readFile(path.join(frontendRoot, 'src/views/openSourceNotice/index.vue'), 'utf8')
|
||||
|
||||
assert.match(noticePage, /RuoYi-Vue3-FastAPI/)
|
||||
assert.match(noticePage, /Copyright \(c\) 2018 RuoYi/)
|
||||
assert.match(noticePage, /The MIT License \(MIT\)/)
|
||||
assert.match(noticePage, /自有业务代码、界面设计和文档不因本声明而开放授权/)
|
||||
})
|
||||
|
||||
test('版权归属与 RuoYi 第三方 MIT 许可范围明确', async () => {
|
||||
const [settings, rootLicense, thirdPartyNotices, packageJson] = await Promise.all([
|
||||
readFile(path.join(frontendRoot, 'src/settings.js'), 'utf8'),
|
||||
readFile(path.join(frontendRoot, '..', 'LICENSE'), 'utf8'),
|
||||
readFile(path.join(frontendRoot, '..', 'THIRD-PARTY-NOTICES.md'), 'utf8'),
|
||||
readFile(path.join(frontendRoot, 'package.json'), 'utf8')
|
||||
])
|
||||
|
||||
assert.match(settings, /Copyright © 2024-2026 Shierfeng International Biotechnology Co\., Ltd\. All Rights Reserved\./)
|
||||
assert.match(rootLicense, /Shierfeng International Biotechnology Co\., Ltd\./)
|
||||
assert.match(rootLicense, /All rights reserved\./i)
|
||||
assert.doesNotMatch(rootLicense, /^MIT License$/m)
|
||||
assert.match(thirdPartyNotices, /RuoYi-Vue3-FastAPI/)
|
||||
assert.match(thirdPartyNotices, /Copyright \(c\) 2018 RuoYi/)
|
||||
assert.match(thirdPartyNotices, /The MIT License \(MIT\)/)
|
||||
assert.equal(JSON.parse(packageJson).license, 'UNLICENSED')
|
||||
})
|
||||
|
||||
const expectedRequests = {
|
||||
'src/api/master/strain.js': [
|
||||
{ url: "'/master/strain/list'", method: 'get' },
|
||||
@@ -72,10 +114,16 @@ const expectedRequests = {
|
||||
{ url: "'/master/pallet'", method: 'post' },
|
||||
{ url: "'/master/pallet'", method: 'put' },
|
||||
],
|
||||
'src/api/master/tunnel.js': [
|
||||
{ url: "'/master/tunnel/list'", method: 'get' },
|
||||
{ url: "'/master/tunnel/' + tunnelId", method: 'get' },
|
||||
{ url: "'/master/tunnel'", method: 'post' },
|
||||
{ url: "'/master/tunnel'", method: 'put' },
|
||||
],
|
||||
'src/api/business/inbound.js': [
|
||||
{ url: "'/business/inbound/list'", method: 'get' },
|
||||
{ url: "'/business/inbound/warehouse-options'", method: 'get' },
|
||||
{ url: "'/business/inbound/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/inbound/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/inbound/' + inboundId", method: 'get' },
|
||||
{ url: "'/business/inbound'", method: 'post' },
|
||||
{ url: "'/business/inbound'", method: 'put' },
|
||||
@@ -84,7 +132,7 @@ const expectedRequests = {
|
||||
'src/api/business/filling.js': [
|
||||
{ url: "'/business/filling/list'", method: 'get' },
|
||||
{ url: "'/business/filling/strain-options'", method: 'get' },
|
||||
{ url: "'/business/filling/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/filling/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/filling/' + fillingId", method: 'get' },
|
||||
{ url: "'/business/filling'", method: 'post' }
|
||||
],
|
||||
@@ -92,8 +140,9 @@ const expectedRequests = {
|
||||
{ url: "'/business/shipment/list'", method: 'get' },
|
||||
{ url: "'/business/shipment/warehouse-options'", method: 'get' },
|
||||
{ url: "'/business/shipment/site-options'", method: 'get' },
|
||||
{ url: "'/business/shipment/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/shipment/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/shipment/' + shipmentId", method: 'get' },
|
||||
{ url: "'/business/shipment/' + shipmentId + '/labels'", method: 'get' },
|
||||
{ url: "'/business/shipment'", method: 'post' },
|
||||
{ url: "'/business/shipment'", method: 'put' },
|
||||
{ url: "'/business/shipment/' + shipmentId + '/confirm'", method: 'put' }
|
||||
@@ -101,14 +150,17 @@ const expectedRequests = {
|
||||
'src/api/business/usage.js': [
|
||||
{ url: "'/business/usage/list'", method: 'get' },
|
||||
{ url: "'/business/usage/line-options'", method: 'get' },
|
||||
{ url: "'/business/usage/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/usage/tunnel-options'", method: 'get' },
|
||||
{ url: "'/business/usage/enter-tunnel'", method: 'post' },
|
||||
{ url: "'/business/usage/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/usage-fifo/recommendation'", method: 'get' },
|
||||
{ url: "'/business/usage/' + usageId", method: 'get' },
|
||||
{ url: "'/business/usage'", method: 'post' }
|
||||
],
|
||||
'src/api/business/return.js': [
|
||||
{ url: "'/business/return/list'", method: 'get' },
|
||||
{ url: "'/business/return/warehouse-options'", method: 'get' },
|
||||
{ url: "'/business/return/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/return/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/return/request'", method: 'post' },
|
||||
{ url: "'/business/return/' + returnId", method: 'get' },
|
||||
{ url: "'/business/return/' + returnId + '/confirm'", method: 'put' }
|
||||
@@ -213,9 +265,11 @@ const businessPageContracts = {
|
||||
payloads: {
|
||||
addFilling: [
|
||||
'batchNo',
|
||||
'expiryDate',
|
||||
'idempotencyKey',
|
||||
'initialPackages',
|
||||
'packageSpec',
|
||||
'productionDate',
|
||||
'qrToken',
|
||||
'strainId',
|
||||
'warehouseId'
|
||||
@@ -228,13 +282,14 @@ const businessPageContracts = {
|
||||
'addShipment',
|
||||
'confirmShipment',
|
||||
'getShipment',
|
||||
'getShipmentLabels',
|
||||
'getShipmentSiteOptions',
|
||||
'getShipmentWarehouseOptions',
|
||||
'listShipment',
|
||||
'scanShipmentPallet',
|
||||
'updateShipment'
|
||||
],
|
||||
permissions: ['add', 'confirm', 'edit', 'list', 'query'],
|
||||
permissions: ['add', 'confirm', 'edit', 'list', 'query', 'receive'],
|
||||
payloads: {
|
||||
addShipment: ['palletIds', 'sourceWarehouseId', 'targetSiteId'],
|
||||
updateShipment: ['palletIds', 'shipmentId', 'sourceWarehouseId', 'targetSiteId'],
|
||||
@@ -243,11 +298,10 @@ const businessPageContracts = {
|
||||
},
|
||||
usage: {
|
||||
relativePath: 'src/views/business/usage/index.vue',
|
||||
apiNames: ['addUsage', 'getUsageLineOptions', 'listUsage', 'scanUsagePallet'],
|
||||
apiNames: ['addUsage', 'enterUsageTunnel', 'getUsageFifoRecommendation', 'getUsageLineOptions', 'getUsageTunnelOptions', 'listUsage', 'scanUsagePallet'],
|
||||
permissions: ['add', 'list', 'query'],
|
||||
payloads: {
|
||||
addUsage: ['idempotencyKey', 'lineId', 'qrToken', 'usedPackages']
|
||||
}
|
||||
// 使用请求需原样持久化以安全重试,载荷边界由 usage-behavior 的真实请求断言验证。
|
||||
payloads: {}
|
||||
},
|
||||
return: {
|
||||
relativePath: 'src/views/business/return/index.vue',
|
||||
@@ -398,6 +452,7 @@ const expectedButtonPermissions = [
|
||||
'business:shipment:add',
|
||||
'business:shipment:edit',
|
||||
'business:shipment:confirm',
|
||||
'business:shipment:receive',
|
||||
'business:usage:query',
|
||||
'business:usage:add',
|
||||
'business:return:query',
|
||||
@@ -1166,8 +1221,8 @@ test('窄屏模式控件仍保留正式退出与测试批次切换入口', async
|
||||
|
||||
test('17 个页面的真实权限门禁逐项匹配独立硬编码契约', async () => {
|
||||
assert.equal(expectedPagePermissions.length, 17)
|
||||
assert.equal(expectedButtonPermissions.length, 37)
|
||||
assert.equal(new Set(expectedPermissions).size, 54)
|
||||
assert.equal(expectedButtonPermissions.length, 38)
|
||||
assert.equal(new Set(expectedPermissions).size, 55)
|
||||
const relativePaths = [
|
||||
...Object.values(masterPageContracts),
|
||||
...Object.values(businessPageContracts),
|
||||
@@ -1467,9 +1522,9 @@ test('首次入库确认失败后取消重试不丢弃原幂等键', async () =>
|
||||
assert.doesNotMatch(confirmCatch, /clearConfirmIdempotency\(\)|handleConfirmCancellation\(/)
|
||||
})
|
||||
|
||||
test('17 个 API 文件逐一匹配硬编码请求契约', async (t) => {
|
||||
assert.equal(Object.keys(expectedRequests).length, 17)
|
||||
assert.equal(Object.values(expectedRequests).flat().length, 66)
|
||||
test('18 个 API 文件逐一匹配硬编码请求契约', async (t) => {
|
||||
assert.equal(Object.keys(expectedRequests).length, 18)
|
||||
assert.equal(Object.values(expectedRequests).flat().length, 74)
|
||||
|
||||
for (const [relativePath, expected] of Object.entries(expectedRequests)) {
|
||||
await t.test(relativePath, async () => {
|
||||
@@ -1482,16 +1537,18 @@ test('17 个 API 文件逐一匹配硬编码请求契约', async (t) => {
|
||||
|
||||
test('关键扫码、选项、确认与二维码契约被显式覆盖', () => {
|
||||
const requiredRequests = [
|
||||
{ url: "'/business/inbound/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/filling/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/shipment/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/usage/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/return/scan/' + qrToken", method: 'get' },
|
||||
{ url: "'/business/inbound/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/filling/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/shipment/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/usage/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/return/scan/' + encodeURIComponent(qrToken)", method: 'get' },
|
||||
{ url: "'/business/inbound/warehouse-options'", method: 'get' },
|
||||
{ url: "'/business/filling/strain-options'", method: 'get' },
|
||||
{ url: "'/business/shipment/warehouse-options'", method: 'get' },
|
||||
{ url: "'/business/shipment/site-options'", method: 'get' },
|
||||
{ url: "'/business/usage/line-options'", method: 'get' },
|
||||
{ url: "'/business/usage/tunnel-options'", method: 'get' },
|
||||
{ url: "'/business/usage/enter-tunnel'", method: 'post' },
|
||||
{ url: "'/business/return/warehouse-options'", method: 'get' },
|
||||
{ url: "'/business/inbound/' + inboundId + '/confirm'", method: 'put' },
|
||||
{ url: "'/business/shipment/' + shipmentId + '/confirm'", method: 'put' },
|
||||
@@ -1960,7 +2017,7 @@ test('五个业务页面逐页满足 API、精确权限、列表和交互基础
|
||||
.map((match) => match[0])
|
||||
assert.ok(businessControls.length > 0, `${resource} 页面缺少业务输入控件`)
|
||||
for (const control of businessControls) {
|
||||
assert.match(control, /:disabled="[^"]*operationPending[^"]*"/, `${resource} 提交期间必须禁用业务输入`)
|
||||
assert.match(control, /(?:\sdisabled(?:\s|\/?>)|:disabled="[^"]*operationPending[^"]*")/, `${resource} 提交期间必须禁用业务输入`)
|
||||
}
|
||||
const businessActions = [...source.matchAll(/<el-button\b[^>]*class="business-action"[^>]*>/g)]
|
||||
.map((match) => match[0])
|
||||
@@ -2018,7 +2075,7 @@ test('五个业务页面的读取请求逐分支执行 stale guard,并在上
|
||||
},
|
||||
usage: {
|
||||
flows: [
|
||||
['loadLineOptions', ['getUsageLineOptions'], 'isCurrentOptionRequest'],
|
||||
['loadUsageOptions', ['getUsageLineOptions', 'getUsageTunnelOptions'], 'isCurrentOptionRequest'],
|
||||
['handleUsageScan', ['scanUsagePallet'], 'isCurrentScanRequest']
|
||||
],
|
||||
resets: [['resetUsageOperation', ['invalidateOptionRequest()', 'invalidateScanRequest()']]],
|
||||
@@ -2172,7 +2229,7 @@ test('装填扫码 pending 期间禁用全部业务输入', async () => {
|
||||
for (const field of ['strainId', 'batchNo', 'packageSpec', 'initialPackages']) {
|
||||
const businessControl = source.match(new RegExp(`<[^>]+v-model="form\\.${field}"[^>]*>`))?.[0]
|
||||
assert.ok(businessControl, `缺少装填 ${field} 业务控件`)
|
||||
assert.match(businessControl, /:disabled="[^"]*scanLoading[^"]*"/, `装填扫码 pending 时必须禁用 ${field}`)
|
||||
assert.match(businessControl, /(?:\sdisabled(?:\s|\/?>)|:disabled="[^"]*scanLoading[^"]*")/, `装填扫码 pending 时必须禁用 ${field}`)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2196,7 +2253,7 @@ test('装填页面锁定扫码上下文、校验正整数并在失败重试时
|
||||
['strainId', 'undefined'],
|
||||
['batchNo', "''"],
|
||||
['packageSpec', "''"],
|
||||
['initialPackages', 'undefined']
|
||||
['initialPackages', '100']
|
||||
]) {
|
||||
const fieldPattern = new RegExp(`(?:form\\.value\\.${field}\\s*=\\s*${emptyValue}|${field}:\\s*${emptyValue})`)
|
||||
assert.match(clearPayloadSource, fieldPattern, `装填载荷清理必须重置 ${field}`)
|
||||
@@ -2366,56 +2423,13 @@ test('发货页面只允许 DRAFT 变更、展示只读余量并复用确认幂
|
||||
assert.doesNotMatch(confirmCatch, /clearConfirmIdempotency\(\)[\s\S]*?(?:loadError|msgError)/)
|
||||
})
|
||||
|
||||
test('使用页面依扫码结果锁定产线并只采用后端返回的余量和状态', async () => {
|
||||
test('使用页面依扫码结果完成入隧、锁定产线并只采用后端返回的余量和状态', async () => {
|
||||
const source = await readFile(path.join(frontendRoot, businessPageContracts.usage.relativePath), 'utf8')
|
||||
|
||||
assertButtonsUsePermission(source, 'submitUsage', 'business:usage:add')
|
||||
assert.match(source, /proxy\.\$auth\.hasPermiAnd\(\['business:usage:query', 'business:usage:add'\]\)/)
|
||||
assert.match(source, /pallet\.currentLineId == null/)
|
||||
assert.match(source, /getUsageLineOptions\(pallet\.siteId\)/)
|
||||
assert.match(source, /:disabled="operationPending \|\| lineLocked/)
|
||||
assert.match(source, /validator:\s*validatePositiveInteger/)
|
||||
|
||||
const scanSource = extractNamedFunction(source, 'handleUsageScan')
|
||||
const scanCallIndex = scanSource.indexOf('scanUsagePallet(')
|
||||
assert.ok(scanCallIndex >= 0, '使用扫码必须调用 scanUsagePallet')
|
||||
for (const [snippet, message] of [
|
||||
['clearUsageIdempotencyKey()', '新使用扫码前必须清除旧幂等键'],
|
||||
['scannedPallet.value = null', '新使用扫码前必须清除旧托盘'],
|
||||
['usageResult.value = null', '新使用扫码前必须清除旧成功结果'],
|
||||
['lineOptions.value = []', '新使用扫码前必须清除旧产线选项'],
|
||||
['lineLocked.value = false', '新使用扫码前必须解除旧产线锁定'],
|
||||
["form.value = { qrToken: '', lineId: undefined, usedPackages: undefined }", '新使用扫码前必须清除旧提交 payload']
|
||||
]) {
|
||||
const index = scanSource.indexOf(snippet)
|
||||
assert.ok(index >= 0 && index < scanCallIndex, message)
|
||||
}
|
||||
const invalidSource = extractNamedFunction(source, 'handleScanInvalid')
|
||||
assert.match(invalidSource, /clearUsageIdempotencyKey\(\)/)
|
||||
assert.match(invalidSource, /scannedPallet\.value = null/)
|
||||
assert.match(invalidSource, /usageResult\.value = null/)
|
||||
assert.match(invalidSource, /lineOptions\.value = \[\]/)
|
||||
assert.match(invalidSource, /lineLocked\.value = false/)
|
||||
assert.match(invalidSource, /form\.value = \{ qrToken: '', lineId: undefined, usedPackages: undefined \}/)
|
||||
|
||||
const submitSource = extractNamedFunction(source, 'submitUsage')
|
||||
assert.ok(
|
||||
submitSource.indexOf('submitLoading.value = true') < submitSource.indexOf('.validate('),
|
||||
'使用提交必须在异步校验前加锁'
|
||||
)
|
||||
assert.match(submitSource, /if \(!valid\) \{\s*submitLoading\.value = false\s*return/)
|
||||
assert.match(submitSource, /\.finally\(\(\) => \{\s*submitLoading\.value = false/)
|
||||
assert.match(submitSource, /const idempotencyKey = ensureUsageIdempotencyKey\(\)/)
|
||||
const successSource = extractPromiseCallback(submitSource, 'then')
|
||||
assert.match(successSource, /const result = response\.data;/)
|
||||
assert.match(successSource, /remainingPackages:\s*result\.remainingPackages/)
|
||||
assert.match(successSource, /flowState:\s*result\.flowState/)
|
||||
assert.doesNotMatch(source, /remainingPackages\s*[-+]=|remainingPackages\s*[-+]\s*form|Math\.(?:max|min)\([^)]*remainingPackages/)
|
||||
assert.match(successSource, /clearUsageIdempotencyKey\(\)/)
|
||||
const catchSource = extractPromiseCallback(submitSource, 'catch')
|
||||
assert.doesNotMatch(catchSource, /clearUsageIdempotencyKey/)
|
||||
assert.match(extractNamedFunction(source, 'resetUsageOperation'), /clearUsageIdempotencyKey\(\)/)
|
||||
assert.match(source, /watch\([\s\S]*?clearUsageIdempotencyKey\(\)/)
|
||||
// 执行真实页面/API行为,避免把“必须每次重扫清空幂等键”的旧实现误当成安全要求。
|
||||
await execFileAsync(process.execPath, ['--test', 'scripts/usage-behavior.test.mjs'], { cwd: frontendRoot })
|
||||
assert.match(source, /@media \(max-width: 767px\)[\s\S]*?min-height:\s*(?:48|52|56)px/)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user