Files
pda-overseas-uniapp/tests/receiving-photo-ui-contract.test.mjs
T
2026-09-15 13:37:44 +08:00

100 lines
6.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
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/)
})
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'\]/)
assert.match(page, /MAX_UNLOAD_PHOTO_BYTES = 10 \* 1024 \* 1024/)
assert.match(page, /tempFiles\?\.\[0\]\?\.size[\s\S]*照片不能超过10MB/)
assert.doesNotMatch(page, /sourceType:\s*\['album'\]|从相册选择|换照片/)
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'/)
assert.match(page, /photoStateLabel\(photo\)[\s\S]*photo\.errorMessage/)
assert.match(page, /this\.showToast\(error\.message \|\| '照片上传失败', 'error'\)/)
assert.match(page, /photoSlots\[index\]\?\.fileId \? '重拍' : '拍照'/)
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/)
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"/)
assert.match(page, /onShow\(\)[\s\S]*this\.inboundMode && \(!this\.bootstrapped \|\| this\.phase === 'candidates'\)[\s\S]*this\.bootstrap\(\)/)
assert.match(page, /await this\.applyCurrentReceiving\(current\);[\s\S]*this\.bootstrapped = true/)
})
test('local thumbnail, preview, and authenticated session restore are wired to the frozen contract', () => {
assert.match(page, /restorePhotoPreviews\(\)[\s\S]*photo\?\.downloadPath[\s\S]*downloadCurrentUnloadPhoto\(photo\.downloadPath\)/)
assert.deepEqual(photoUiBlockers().map((item) => item.code), [])
assert.ok(photoUiBlockers().every((item) => item.deploymentBlocker === true && item.reason))
})
test('domestic cabinet photos are read-only, previewable and non-blocking in sign and receive phases', () => {
assert.match(page, /国内装柜照片(只读)/)
assert.match(page, /domesticPhotosExpanded/)
assert.match(page, /downloadCurrentDomesticLoadingPhoto\(photo\.downloadPath\)/)
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/)
})
test('fixed receiving action leaves enough scroll-safe space above the PDA bottom bar', () => {
assert.match(page, /v-if="canFinishOnsite" class="sticky-action one"/)
assert.match(page, /allInboundsReconciled \? '完成整柜入库' : '现场收货结束(标记差异)'/)
assert.match(page, /receiving-content-with-action\{[^}]*padding-bottom:calc\(152px \+ env\(safe-area-inset-bottom,0px\)\)/)
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/)
})