功能:新增业务基础资料页面
This commit is contained in:
@@ -126,6 +126,51 @@ const expectedReportExports = {
|
||||
'src/api/report/palletTrace.js': ['listPalletTrace']
|
||||
}
|
||||
|
||||
const masterPageContracts = {
|
||||
strain: {
|
||||
relativePath: 'src/views/master/strain/index.vue',
|
||||
apiNames: ['addStrain', 'delStrain', 'getStrain', 'listStrain', 'updateStrain'],
|
||||
permissions: ['add', 'edit', 'list', 'query', 'remove'],
|
||||
addPayloadKeys: ['remark', 'status', 'strainCode', 'strainName'],
|
||||
editPayloadKeys: ['remark', 'status', 'strainCode', 'strainId', 'strainName']
|
||||
},
|
||||
customer: {
|
||||
relativePath: 'src/views/master/customer/index.vue',
|
||||
apiNames: ['addCustomer', 'delCustomer', 'getCustomer', 'listCustomer', 'updateCustomer'],
|
||||
permissions: ['add', 'edit', 'list', 'query', 'remove'],
|
||||
addPayloadKeys: ['customerCode', 'customerName', 'remark', 'status'],
|
||||
editPayloadKeys: ['customerCode', 'customerId', 'customerName', 'remark', 'status']
|
||||
},
|
||||
site: {
|
||||
relativePath: 'src/views/master/site/index.vue',
|
||||
apiNames: ['addSite', 'delSite', 'getSite', 'listSite', 'updateSite'],
|
||||
permissions: ['add', 'assign', 'edit', 'list', 'query', 'remove'],
|
||||
addPayloadKeys: ['address', 'customerId', 'remark', 'siteCode', 'siteName', 'status'],
|
||||
editPayloadKeys: ['address', 'customerId', 'remark', 'siteCode', 'siteId', 'siteName', 'status']
|
||||
},
|
||||
line: {
|
||||
relativePath: 'src/views/master/line/index.vue',
|
||||
apiNames: ['addLine', 'delLine', 'getLine', 'listLine', 'updateLine'],
|
||||
permissions: ['add', 'edit', 'list', 'query', 'remove'],
|
||||
addPayloadKeys: ['lineCode', 'lineName', 'remark', 'siteId', 'status'],
|
||||
editPayloadKeys: ['lineCode', 'lineId', 'lineName', 'remark', 'siteId', 'status']
|
||||
},
|
||||
warehouse: {
|
||||
relativePath: 'src/views/master/warehouse/index.vue',
|
||||
apiNames: ['addWarehouse', 'delWarehouse', 'getWarehouse', 'listWarehouse', 'updateWarehouse'],
|
||||
permissions: ['add', 'assign', 'edit', 'list', 'query', 'remove'],
|
||||
addPayloadKeys: ['address', 'remark', 'status', 'warehouseCode', 'warehouseName'],
|
||||
editPayloadKeys: ['address', 'remark', 'status', 'warehouseCode', 'warehouseId', 'warehouseName']
|
||||
},
|
||||
pallet: {
|
||||
relativePath: 'src/views/master/pallet/index.vue',
|
||||
apiNames: ['addPallet', 'delPallet', 'getPallet', 'getPalletQrcode', 'listPallet', 'updatePallet'],
|
||||
permissions: ['add', 'edit', 'list', 'print', 'query', 'remove'],
|
||||
addPayloadKeys: ['capacityPackages', 'palletCode', 'remark'],
|
||||
editPayloadKeys: ['assetStatus', 'capacityPackages', 'palletId', 'remark']
|
||||
}
|
||||
}
|
||||
|
||||
const delimiterPairs = { '(': ')', '[': ']', '{': '}' }
|
||||
|
||||
function sanitizeSource(source) {
|
||||
@@ -301,6 +346,44 @@ function extractRequests(source) {
|
||||
return requests
|
||||
}
|
||||
|
||||
function extractImportedNames(source, modulePath) {
|
||||
const escapedPath = modulePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const importMatch = new RegExp(`import\\s*{([^}]*)}\\s*from\\s*['"]${escapedPath}['"]`).exec(source)
|
||||
assert.ok(importMatch, `缺少 ${modulePath} 的具名导入`)
|
||||
return importMatch[1]
|
||||
.split(',')
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
}
|
||||
|
||||
function extractCallObjectKeys(source, callName) {
|
||||
const escapedCallName = callName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const callMatch = new RegExp(`(?<![\\w$])${escapedCallName}\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`).exec(source)
|
||||
assert.ok(callMatch, `缺少 ${callName}({ ... }) 显式对象调用`)
|
||||
|
||||
const properties = callMatch[1].split(',').map((property) => property.trim()).filter(Boolean)
|
||||
return properties.map((property) => {
|
||||
const keyMatch = /^([A-Za-z_$][\w$]*)\s*:/.exec(property)
|
||||
assert.ok(keyMatch, `${callName} payload 禁止展开、简写或动态属性`)
|
||||
return keyMatch[1]
|
||||
}).sort()
|
||||
}
|
||||
|
||||
function extractButtonBlocks(source, clickHandler) {
|
||||
return [...source.matchAll(/<el-button\b[\s\S]*?<\/el-button>/g)]
|
||||
.map((match) => match[0])
|
||||
.filter((button) => button.includes(`@click="${clickHandler}`))
|
||||
}
|
||||
|
||||
function assertButtonsUsePermission(source, clickHandler, permission) {
|
||||
const buttons = extractButtonBlocks(source, clickHandler)
|
||||
assert.ok(buttons.length > 0, `缺少 ${clickHandler} 按钮`)
|
||||
for (const button of buttons) {
|
||||
assert.match(button, new RegExp(`v-hasPermi="\\['${permission}'\\]"`), `${clickHandler} 权限不精确`)
|
||||
}
|
||||
}
|
||||
|
||||
function hasExpectedRequest(expectedRequest) {
|
||||
return Object.values(expectedRequests).some((requests) =>
|
||||
requests.some((request) =>
|
||||
@@ -475,3 +558,150 @@ test('扫码组件声明交互契约并保持触控尺寸', async () => {
|
||||
assert.match(source, /focus-within/)
|
||||
assert.doesNotMatch(source, /BarcodeDetector|camera|getUserMedia/i)
|
||||
})
|
||||
|
||||
test('六个基础资料页面逐页满足 API、权限、分页和安全删除契约', async (t) => {
|
||||
for (const [resource, contract] of Object.entries(masterPageContracts)) {
|
||||
await t.test(contract.relativePath, async () => {
|
||||
const source = await readFile(path.join(frontendRoot, contract.relativePath), 'utf8')
|
||||
const permissionPrefix = `master:${resource}`
|
||||
const importedModules = [...source.matchAll(/from\s*['"]@\/api\/master\/([a-z]+)['"]/g)]
|
||||
.map((match) => match[1])
|
||||
|
||||
assert.deepEqual(importedModules, [resource], `${resource} 页面不得跨调其他主数据 API`)
|
||||
assert.deepEqual(extractImportedNames(source, `@/api/master/${resource}`), contract.apiNames)
|
||||
|
||||
const actualPermissions = [...new Set(
|
||||
[...source.matchAll(new RegExp(`['"](${permissionPrefix}:[a-z]+)['"]`, 'g'))]
|
||||
.map((match) => match[1].slice(permissionPrefix.length + 1))
|
||||
)].sort()
|
||||
assert.deepEqual(actualPermissions, contract.permissions, `${resource} 页面权限集合不精确`)
|
||||
|
||||
assertButtonsUsePermission(source, 'handleQuery', `${permissionPrefix}:list`)
|
||||
assertButtonsUsePermission(source, 'resetQuery', `${permissionPrefix}:list`)
|
||||
assertButtonsUsePermission(source, 'handleAdd', `${permissionPrefix}:add`)
|
||||
assertButtonsUsePermission(source, 'submitCreate', `${permissionPrefix}:add`)
|
||||
assertButtonsUsePermission(source, 'handleUpdate', `${permissionPrefix}:edit`)
|
||||
assertButtonsUsePermission(source, 'submitUpdate', `${permissionPrefix}:edit`)
|
||||
assertButtonsUsePermission(source, 'handleDelete', `${permissionPrefix}:remove`)
|
||||
|
||||
for (const button of extractButtonBlocks(source, 'handleUpdate')) {
|
||||
assert.match(
|
||||
button,
|
||||
new RegExp(`v-if="proxy\\.\\$auth\\.hasPermiAnd\\(\\['${permissionPrefix}:query', '${permissionPrefix}:edit'\\]\\)"`),
|
||||
`${resource} 修改按钮必须同时校验 query 与 edit`
|
||||
)
|
||||
}
|
||||
|
||||
assert.match(source, /proxy\.useDict\(['"]sys_normal_disable['"]\)/)
|
||||
assert.match(source, /<dict-tag\s+:options="sys_normal_disable"/)
|
||||
assert.match(source, /\.value\s*=\s*response\.rows/)
|
||||
assert.match(source, /total\.value\s*=\s*response\.total/)
|
||||
assert.match(source, /const detail\s*=\s*response\.data/)
|
||||
assert.match(source, /function handleQuery\(\)\s*{[\s\S]*?queryParams\.value\.pageNum\s*=\s*1[\s\S]*?getList\(\)/)
|
||||
assert.match(source, /function resetQuery\(\)\s*{[\s\S]*?resetForm\(['"]queryRef['"]\)[\s\S]*?handleQuery\(\)/)
|
||||
assert.match(source, /<pagination\b[\s\S]*?v-model:page="queryParams\.pageNum"[\s\S]*?v-model:limit="queryParams\.pageSize"[\s\S]*?@pagination="getList"/)
|
||||
assert.match(source, /<el-table\b[^>]*v-loading="loading"[^>]*empty-text="暂无数据"/)
|
||||
assert.match(source, /<el-alert\b[^>]*v-if="loadError"/)
|
||||
assert.match(source, /<el-alert\b[^>]*v-if="formError"/)
|
||||
assert.match(source, /const formError = ref\(['"]['"]\)/)
|
||||
assert.doesNotMatch(source, /\.catch\(\(\) => \{\}\)/, `${resource} 页面禁止空 catch`)
|
||||
assert.doesNotMatch(source, />\s*(?:导入|导出)\s*<\/el-button>/)
|
||||
assert.doesNotMatch(source, /\bhandle(?:Import|Export)\b|proxy\.download\(/)
|
||||
|
||||
assert.match(source, /<el-table-column\b[^>]*type="selection"[^>]*:selectable="isRowSelectable"/)
|
||||
assert.match(source, /function isRowSelectable\(row\)\s*{\s*return row\.canRemove === true;?\s*}/)
|
||||
assert.match(source, /ids\.value\s*=\s*selection\s*\.filter\(\(item\) => item\.canRemove === true\)/)
|
||||
const rowDeleteButtons = extractButtonBlocks(source, 'handleDelete')
|
||||
.filter((button) => button.includes('handleDelete(scope.row)'))
|
||||
assert.ok(rowDeleteButtons.length > 0, `${resource} 缺少行删除按钮`)
|
||||
for (const button of rowDeleteButtons) {
|
||||
assert.match(button, /v-if="scope\.row\.canRemove === true"/)
|
||||
}
|
||||
|
||||
const suffix = resource[0].toUpperCase() + resource.slice(1)
|
||||
assert.deepEqual(extractCallObjectKeys(source, `add${suffix}`), contract.addPayloadKeys)
|
||||
assert.deepEqual(extractCallObjectKeys(source, `update${suffix}`), contract.editPayloadKeys)
|
||||
|
||||
assert.match(source, /validator:\s*validateRequiredText/)
|
||||
assert.match(source, /maxlength="500"/)
|
||||
if (resource !== 'pallet') {
|
||||
assert.match(source, /maxlength="64"/)
|
||||
assert.match(source, /maxlength="100"/)
|
||||
}
|
||||
if (resource === 'site' || resource === 'line' || resource === 'pallet') {
|
||||
assert.match(source, /validator:\s*validatePositiveInteger/)
|
||||
}
|
||||
|
||||
if (resource === 'site' || resource === 'warehouse') {
|
||||
assertButtonsUsePermission(source, 'handleAssign', `${permissionPrefix}:assign`)
|
||||
assert.match(source, /import BusinessUserAssignDialog from ['"]@\/components\/BusinessUserAssignDialog['"]/)
|
||||
assert.match(source, /<business-user-assign-dialog\s+ref="assignDialogRef"\s+@ok="getList"\s*\/>/)
|
||||
assert.match(
|
||||
source,
|
||||
new RegExp(`assignDialogRef\\.value\\.show\\(\\{[\\s\\S]*?resourceType: ['"]${resource}['"][\\s\\S]*?resourceId: row\\.${resource}Id[\\s\\S]*?resourceName: row\\.${resource}Name[\\s\\S]*?}\\)`)
|
||||
)
|
||||
}
|
||||
|
||||
if (resource === 'pallet') {
|
||||
assertButtonsUsePermission(source, 'handlePrint', 'master:pallet:print')
|
||||
const printHandler = source.slice(source.indexOf('function handlePrint'))
|
||||
assert.match(printHandler, /const printWindow = window\.open\('', '_blank'\)/)
|
||||
assert.ok(
|
||||
printHandler.indexOf("window.open('', '_blank')") < printHandler.indexOf('getPalletQrcode('),
|
||||
'打印窗口必须在异步二维码请求前同步打开'
|
||||
)
|
||||
assert.match(printHandler, /getPalletQrcode\(row\.palletId\)/)
|
||||
assert.match(printHandler, /instanceof Blob/)
|
||||
assert.match(printHandler, /URL\.createObjectURL\(/)
|
||||
assert.match(printHandler, /URL\.revokeObjectURL\(/)
|
||||
assert.match(printHandler, /document\.createElement\(['"](?:h1|img|style)['"]\)/)
|
||||
assert.match(printHandler, /\.textContent\s*=/)
|
||||
assert.match(printHandler, /['"]load['"][\s\S]*?printWindow\.print\(\)/)
|
||||
assert.match(printHandler, /['"]afterprint['"][\s\S]*?URL\.revokeObjectURL/)
|
||||
assert.doesNotMatch(printHandler, /document\.write|innerHTML|BarcodeDetector|getUserMedia|camera/i)
|
||||
assert.match(source, /v-if="isEdit"[\s\S]*?v-model="form\.palletCode"[\s\S]*?readonly/)
|
||||
assert.match(source, /v-if="isEdit"[\s\S]*?v-model="form\.qrToken"[\s\S]*?readonly/)
|
||||
assert.doesNotMatch(contract.editPayloadKeys.join(','), /palletCode|qrToken/)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('业务用户分配组件使用固定映射并允许空数组完整替换', async () => {
|
||||
const relativePath = 'src/components/BusinessUserAssignDialog/index.vue'
|
||||
const source = await readFile(path.join(frontendRoot, relativePath), 'utf8')
|
||||
|
||||
assert.deepEqual(
|
||||
extractImportedNames(source, '@/api/master/site'),
|
||||
['assignSiteUsers', 'getSiteUsers']
|
||||
)
|
||||
assert.deepEqual(
|
||||
extractImportedNames(source, '@/api/master/warehouse'),
|
||||
['assignWarehouseUsers', 'getWarehouseUsers']
|
||||
)
|
||||
assert.match(source, /const resourceApiMap = Object\.freeze\(\{[\s\S]*?site:\s*Object\.freeze\(\{\s*getUsers:\s*getSiteUsers,\s*assignUsers:\s*assignSiteUsers\s*}\)[\s\S]*?warehouse:\s*Object\.freeze\(\{\s*getUsers:\s*getWarehouseUsers,\s*assignUsers:\s*assignWarehouseUsers\s*}\)[\s\S]*?}\)/)
|
||||
assert.doesNotMatch(source, /defineProps\(/)
|
||||
assert.match(source, /function show\(\{ resourceType, resourceId, resourceName }\)/)
|
||||
assert.match(source, /response\.data/)
|
||||
assert.match(source, /userId:\s*row\.userId,\s*userName:\s*row\.userName,\s*nickName:\s*row\.nickName,\s*assigned:\s*row\.assigned === true/)
|
||||
assert.doesNotMatch(source, /\.\.\.row|email:\s*row\.|phonenumber:\s*row\.|status:\s*row\./)
|
||||
assert.match(source, /filteredUsers\s*=\s*computed\([\s\S]*?user\.userName[\s\S]*?user\.nickName/)
|
||||
assert.match(source, /nextTick\([\s\S]*?toggleRowSelection\(row, selectedUserIds\.value\.includes\(row\.userId\)\)/)
|
||||
assert.match(source, /@row-click="handleRowClick"/)
|
||||
assert.match(source, /const userIds = \[\.\.\.new Set\(selectedUserIds\.value\.filter\(\(userId\) => Number\.isInteger\(userId\) && userId > 0\)\)\]/)
|
||||
assert.match(source, /assignUsers\(resourceId\.value, \{ userIds }\)/)
|
||||
assert.doesNotMatch(source, /请选择要分配的用户|userIds\.length\s*===?\s*0/)
|
||||
assert.match(source, /:loading="submitLoading"/)
|
||||
assert.match(source, /const loadFailed = ref\(false\)/)
|
||||
assert.match(source, /:disabled="loading \|\| submitLoading \|\| loadFailed"/)
|
||||
assert.doesNotMatch(source, /\.catch\(\(\) => \{\}\)/, '分配组件禁止空 catch')
|
||||
const submitButtons = extractButtonBlocks(source, 'submitAssignment')
|
||||
const submitPermissions = submitButtons.flatMap((button) =>
|
||||
[...button.matchAll(/v-hasPermi="\['(master:(?:site|warehouse):assign)'\]"/g)].map((match) => match[1])
|
||||
).sort()
|
||||
assert.deepEqual(submitPermissions, ['master:site:assign', 'master:warehouse:assign'])
|
||||
assert.match(source, /proxy\.\$modal\.msgSuccess\(response\.msg \|\| ['"]分配成功['"]\)/)
|
||||
assert.match(source, /visible\.value = false[\s\S]*?emit\(['"]ok['"]\)/)
|
||||
assert.match(source, /<el-alert\b[^>]*v-if="loadError"/)
|
||||
assert.match(source, /width="min\(720px, 92vw\)"/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user