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 = / 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/) })