2026-08-26 15:59:08 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import test from 'node:test'
|
2026-11-06 12:32:22 +08:00
|
|
|
import { parse, compileStyle, compileTemplate } from '@vue/compiler-sfc'
|
2026-08-26 15:59:08 +08:00
|
|
|
|
|
|
|
|
const read = (relative) => readFileSync(new URL(relative, import.meta.url), 'utf8')
|
|
|
|
|
const page = read('../src/pages/operation/operation.vue')
|
|
|
|
|
const adapter = read('../src/api/receiving-adapter.js')
|
|
|
|
|
const inbound = read('../src/api/inbound.js')
|
|
|
|
|
const controller = read('../../../baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerController.java')
|
|
|
|
|
const openapi = read('../../../baiyue-firstleg/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml')
|
|
|
|
|
|
|
|
|
|
function photoUiBlockers() {
|
|
|
|
|
const values = []
|
|
|
|
|
const hasThumbnail = /<image[\s\S]*photoSlots\[index\]/.test(page)
|
|
|
|
|
const hasPreview = /uni\.previewImage\(/.test(page)
|
|
|
|
|
const hasRestoreDownload = /@GetMapping\([^\n]*unload-photos/.test(controller)
|
|
|
|
|
|| /receiving-containers\/current\/unload-photos\/files\/\{fileId\}/.test(openapi)
|
|
|
|
|
const hasRestoreClient = /downloadCurrentUnloadPhoto/.test(inbound) && /downloadPath/.test(page)
|
|
|
|
|
const developmentLabels = ['真实接口', '待 PDA 接口', 'Mock']
|
|
|
|
|
.filter((label) => page.toLowerCase().includes(label.toLowerCase()))
|
|
|
|
|
if (!hasThumbnail || !hasPreview) values.push({
|
|
|
|
|
code: 'PHOTO_THUMBNAIL_PREVIEW_MISSING', deploymentBlocker: true,
|
|
|
|
|
reason: '上传成功后只显示完成图标,尚未渲染缩略图或调用uni.previewImage',
|
|
|
|
|
})
|
|
|
|
|
if (!hasRestoreDownload) values.push({
|
|
|
|
|
code: 'PHOTO_RESTORE_DOWNLOAD_ENDPOINT_MISSING', deploymentBlocker: true,
|
|
|
|
|
reason: 'current恢复只返回fileId/role,后端没有签收照片鉴权下载端点,无法恢复后回显',
|
|
|
|
|
})
|
|
|
|
|
else if (!hasRestoreClient) values.push({
|
|
|
|
|
code: 'PHOTO_RESTORE_CLIENT_PENDING', deploymentBlocker: true,
|
|
|
|
|
reason: '后端下载契约仍在冻结中,前端尚未接入重载后的鉴权照片回显',
|
|
|
|
|
})
|
|
|
|
|
if (developmentLabels.length) values.push({
|
|
|
|
|
code: 'DEVELOPMENT_LABELS_VISIBLE', deploymentBlocker: true,
|
|
|
|
|
reason: `页面仍含开发标识:${developmentLabels.join(',')}`,
|
|
|
|
|
})
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('three fixed receiving photo roles and string file IDs are frozen', () => {
|
|
|
|
|
assert.match(adapter, /RECEIVING_PHOTO_ROLES = \['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN'\]/)
|
|
|
|
|
assert.match(adapter, /fileId: String\(photo\.fileId\)/)
|
|
|
|
|
assert.match(page, /fileId: String\(file\.fileId\)/)
|
|
|
|
|
assert.match(page, /fileId: String\(item\.fileId\)/)
|
|
|
|
|
assert.doesNotMatch(inbound, /customerId|planNo|allocationId|warehouseId|locationId/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-26 16:50:25 +08:00
|
|
|
test('camera replaces only one fixed slot and failed retake preserves its previous success', () => {
|
|
|
|
|
assert.match(page, /choosePhoto\(index\)[\s\S]*uni\.chooseImage\(\{ count: 1, sourceType: \['camera'\]/)
|
2026-08-28 12:33:17 +08:00
|
|
|
assert.match(page, /MAX_UNLOAD_PHOTO_BYTES = 10 \* 1024 \* 1024/)
|
|
|
|
|
assert.match(page, /tempFiles\?\.\[0\]\?\.size[\s\S]*照片不能超过10MB/)
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.doesNotMatch(page, /sourceType:\s*\['album'\]|从相册选择|换照片/)
|
2026-08-26 15:59:08 +08:00
|
|
|
assert.match(page, /async replacePhoto\(index, path\)[\s\S]*const previous = receiving\.photoSlots\[index\]/)
|
|
|
|
|
assert.match(page, /await uploadCurrentUnloadPhoto\(path\)[\s\S]*fileId: String\(file\.fileId\)[\s\S]*localPath: path/)
|
|
|
|
|
assert.match(page, /previous\?\.fileId \? \{ \.\.\.previous, status: 'error'/)
|
|
|
|
|
assert.match(page, /status: 'uploading'/)
|
|
|
|
|
assert.match(page, /status: 'error'/)
|
2026-08-28 12:33:17 +08:00
|
|
|
assert.match(page, /photoStateLabel\(photo\)[\s\S]*photo\.errorMessage/)
|
|
|
|
|
assert.match(page, /this\.showToast\(error\.message \|\| '照片上传失败', 'error'\)/)
|
2026-08-26 17:09:07 +08:00
|
|
|
assert.match(page, /photoSlots\[index\]\?\.fileId \? '重拍' : '拍照'/)
|
2026-08-26 15:59:08 +08:00
|
|
|
assert.match(page, /previewPhoto\(index\)/)
|
|
|
|
|
assert.match(page, /uploadedPhotoCount\(\) \{ return this\.photoSlots\.filter\(\(item\) => item\?\.fileId\)\.length \}/)
|
|
|
|
|
assert.match(page, /photosReady\(\) \{ return this\.uploadedPhotoCount === 3 && !this\.photoSlots\.some/)
|
|
|
|
|
assert.match(page, /:disabled="busy \|\| !photosReady" @tap="signContainer"/)
|
|
|
|
|
assert.match(page, /async signContainer\(\) \{ if \(!this\.photosReady\) return;/)
|
|
|
|
|
assert.match(page, /const photos = this\.photoSlots\.map\(/)
|
|
|
|
|
assert.doesNotMatch(page, /count:\s*9|flatMap\(|photoSlots\[index\]\.push/)
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.match(page, /\.photo-grid\{display:grid;grid-template-columns:1fr/)
|
|
|
|
|
assert.match(page, /grid-template-columns:82px minmax\(0,1fr\) 78px/)
|
|
|
|
|
assert.match(page, /class="photo-role-info"/)
|
2026-10-02 11:12:56 +08:00
|
|
|
assert.match(page, /onShow\(\)[\s\S]*this\.inboundMode && \(!this\.bootstrapped \|\| this\.phase === 'candidates' \|\| this\._resumeReceiving\)[\s\S]*this\.bootstrap\(\)/)
|
2026-08-27 15:56:13 +08:00
|
|
|
assert.match(page, /await this\.applyCurrentReceiving\(current\);[\s\S]*this\.bootstrapped = true/)
|
2026-08-26 15:59:08 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('local thumbnail, preview, and authenticated session restore are wired to the frozen contract', () => {
|
2026-10-02 11:12:56 +08:00
|
|
|
assert.match(page, /restorePhotoPreviews\(\)[\s\S]*photo\?\.downloadPath[\s\S]*restoreReceivingPhoto\(photo, downloadCurrentUnloadPhoto\)/)
|
2026-08-26 15:59:08 +08:00
|
|
|
assert.deepEqual(photoUiBlockers().map((item) => item.code), [])
|
|
|
|
|
assert.ok(photoUiBlockers().every((item) => item.deploymentBlocker === true && item.reason))
|
|
|
|
|
})
|
2026-08-26 16:50:25 +08:00
|
|
|
|
2026-09-15 13:37:44 +08:00
|
|
|
test('domestic cabinet photos are read-only, previewable and non-blocking in sign and receive phases', () => {
|
|
|
|
|
assert.match(page, /国内装柜照片(只读)/)
|
|
|
|
|
assert.match(page, /domesticPhotosExpanded/)
|
2026-10-02 11:12:56 +08:00
|
|
|
assert.match(page, /restoreReceivingPhoto\(photo, downloadCurrentDomesticLoadingPhoto\)/)
|
2026-09-15 13:37:44 +08:00
|
|
|
assert.match(page, /async loadDomesticPhoto\(photo\)[\s\S]*catch \(_\) \{ photo\.status = 'error' \}/)
|
|
|
|
|
assert.match(page, /previewDomesticPhoto\(photo\)[\s\S]*uni\.previewImage/)
|
|
|
|
|
assert.match(inbound, /downloadCurrentDomesticLoadingPhoto/)
|
|
|
|
|
assert.match(controller, /current\/domestic-loading-photos\/\{evidenceType\}\/content/)
|
|
|
|
|
assert.match(openapi, /DomesticLoadingPhoto:/)
|
|
|
|
|
assert.doesNotMatch(page, /domesticLoadingPhotos[\s\S]{0,120}customer/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-11-05 16:03:22 +08:00
|
|
|
test('receiving finish action remains in document flow after the scan and save controls', () => {
|
2026-11-06 12:32:22 +08:00
|
|
|
assert.match(page, /v-if="canFinishOnsite" class="receiving-finish-card"/)
|
2026-09-10 20:27:04 +08:00
|
|
|
assert.match(page, /allInboundsReconciled \? '完成整柜入库' : '现场收货结束(标记差异)'/)
|
2026-11-06 12:32:22 +08:00
|
|
|
assert.ok(page.indexOf('class="receiving-finish-card"') > page.indexOf('@tap="submitReceipt"'))
|
|
|
|
|
assert.match(page, /:class="allInboundsReconciled \? 'primary-button' : 'secondary-button'"/)
|
|
|
|
|
assert.match(page, /receiving-content\{[^}]*padding-bottom:calc\(24px \+ env\(safe-area-inset-bottom,0px\)\)/)
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.match(page, /primary-button,\.secondary-button,\.ghost-button\{[^}]*align-items:center;justify-content:center[^}]*line-height:normal;text-align:center/)
|
|
|
|
|
assert.match(page, /sticky-action button\{[^}]*align-items:center;justify-content:center;line-height:normal;text-align:center/)
|
2026-11-06 12:32:22 +08:00
|
|
|
const { descriptor, errors } = parse(page)
|
|
|
|
|
assert.deepEqual(errors, [])
|
|
|
|
|
const style = compileStyle({ source: descriptor.styles[0].content, filename: 'operation.vue', id: 'receiving', scoped: true })
|
|
|
|
|
assert.deepEqual(style.errors, [])
|
|
|
|
|
const dockPositions = []
|
|
|
|
|
style.rawResult.root.walkRules(rule => {
|
|
|
|
|
if (rule.selector.includes('.sticky-action-dock')) rule.walkDecls('position', declaration => dockPositions.push(declaration.value))
|
|
|
|
|
if (rule.selector.includes('.receiving-finish-card')) rule.walkDecls('position', declaration => assert.equal(declaration.value, 'static'))
|
|
|
|
|
})
|
|
|
|
|
assert.deepEqual(dockPositions, ['static'])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('pallet quantity input reserves room above the keyboard and confirms through receipt submission', () => {
|
|
|
|
|
const { descriptor, errors } = parse(page)
|
|
|
|
|
assert.deepEqual(errors, [])
|
|
|
|
|
const compiled = compileTemplate({ source: descriptor.template.content, filename: 'operation.vue', id: 'receiving' })
|
|
|
|
|
assert.deepEqual(compiled.errors, [])
|
|
|
|
|
const quantityInput = descriptor.template.content.match(/<input[^>]*class="quantity-input"[^>]*>/)?.[0] || ''
|
|
|
|
|
assert.match(quantityInput, /v-model\.number="packageCount"/)
|
|
|
|
|
assert.match(quantityInput, /type="number" inputmode="numeric"/)
|
|
|
|
|
assert.match(quantityInput, /:adjust-position="true" :cursor-spacing="120" :auto-blur="true"/)
|
|
|
|
|
assert.match(quantityInput, /@focus="activeTarget = 'quantity'" @confirm="submitReceipt"/)
|
|
|
|
|
assert.match(page, /class="quantity-unit">箱/)
|
2026-08-26 16:50:25 +08:00
|
|
|
})
|