发布当前测试版本至临时灰度环境 #11

Merged
admin merged 43 commits from release/test into main 2026-10-07 11:11:54 +08:00
2 changed files with 98 additions and 8 deletions
Showing only changes of commit 1e707f613c - Show all commits
@@ -0,0 +1,71 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import ts from 'typescript'
import * as vue from 'vue'
function deferred() { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no }); return { promise, resolve, reject } }
function harness(t) {
const source = readFileSync(new URL('./CustomsDocumentsDialog.vue', import.meta.url), 'utf8').match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
const code = ts.transpileModule(source + '\nexport { loadDetail, detail, clearanceResults, loading, errorMessage, selectedInboundIds }', { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
const props = vue.reactive({ modelValue: false, orderId: 'A', canEdit: true }), requests = [], clearances = [], updates = [], view = {}
const unmounts = []
const dependencies = { vue: { ...vue, onBeforeUnmount: fn => unmounts.push(fn) }, 'element-plus': { ElMessage: {} }, '@/modules/first-leg/api/customs': {
getMerchantCustomsForOrder: id => { const pending = deferred(); requests.push({ id, ...pending }); return pending.promise },
getMerchantClearanceResults: id => { const pending = deferred(); clearances.push({ id, ...pending }); return pending.promise },
merchantCustomsErrorMessage: error => error.message,
} }
const scope = vue.effectScope()
scope.run(() => new Function('require', 'exports', 'defineProps', 'defineEmits', code)(id => {
assert.ok(id in dependencies, id); return dependencies[id]
}, view, () => props, () => (...args) => updates.push(args)))
t.after(() => scope.stop())
return { props, requests, clearances, updates, view, unmount: () => unmounts.forEach(fn => fn()) }
}
const detail = id => ({ loadingPlanId: id, inbounds: [{ orderId: id, inboundId: `IN-${id}`, taxMode: 'TAX_REFUND' }] })
const tick = async () => { await Promise.resolve(); await vue.nextTick(); await Promise.resolve() }
test('switching orders while open starts the new request and ignores stale detail', async t => {
const h = harness(t); h.props.modelValue = true; await tick()
h.props.orderId = 'B'; await tick()
assert.deepEqual(h.requests.map(r => r.id), ['A', 'B'])
h.requests[1].resolve(detail('B')); await tick(); h.clearances[0].resolve([]); await tick()
h.requests[0].resolve(detail('A')); await tick()
assert.equal(h.view.detail.value.loadingPlanId, 'B')
assert.deepEqual(h.view.selectedInboundIds.value, ['IN-B'])
assert.deepEqual(h.clearances.map(r => r.id), ['B'])
assert.equal(h.updates.length, 1)
})
test('closing before detail response prevents stale data and events', async t => {
const h = harness(t); h.props.modelValue = true; await tick()
h.props.modelValue = false; await tick()
h.requests[0].resolve(detail('A')); await tick()
assert.equal(h.view.detail.value, null); assert.equal(h.view.loading.value, false)
assert.equal(h.clearances.length, 0); assert.equal(h.updates.length, 0)
})
test('closing and reopening ignores the previous clearance response', async t => {
const h = harness(t); h.props.modelValue = true; await tick()
h.requests[0].resolve(detail('A')); await tick()
h.props.modelValue = false; await tick(); h.props.orderId = 'B'; h.props.modelValue = true; await tick()
h.requests[1].resolve(detail('B')); await tick()
h.clearances[1].resolve([{ kind: 'B' }]); await tick()
h.clearances[0].resolve([{ kind: 'A' }]); await tick()
assert.deepEqual(h.view.clearanceResults.value, [{ kind: 'B' }])
assert.equal(h.view.detail.value.loadingPlanId, 'B'); assert.equal(h.updates.length, 1)
})
test('stale detail failure cannot clear the next request loading or show an error', async t => {
const h = harness(t); h.props.modelValue = true; await tick()
h.props.orderId = 'B'; await tick(); h.requests[0].reject(new Error('old failure')); await tick()
assert.equal(h.view.loading.value, true); assert.equal(h.view.errorMessage.value, '')
h.requests[1].resolve(detail('B')); await tick(); h.clearances[0].resolve([]); await tick()
assert.equal(h.view.loading.value, false); assert.equal(h.view.detail.value.loadingPlanId, 'B')
})
test('unmount invalidates pending detail requests', async t => {
const h = harness(t); h.props.modelValue = true; await tick(); h.unmount()
h.requests[0].resolve(detail('A')); await tick()
assert.equal(h.clearances.length, 0); assert.equal(h.updates.length, 0); assert.equal(h.view.detail.value, null)
})
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Download, FileText, LockKeyhole, Pencil, Trash2, Upload } from '@lucide/vue'
import {
@@ -46,6 +46,8 @@ const replaceInput = ref<HTMLInputElement>()
const mutating = ref(false)
const downloadingKey = ref('')
const clearanceResults = ref<MerchantClearanceResult[]>([])
let detailRequest = 0
onBeforeUnmount(() => { detailRequest += 1 })
const taxRefundInbounds = computed(() => detail.value?.inbounds.filter((item) => item.taxMode === 'TAX_REFUND') ?? [])
const locked = computed(() => Boolean(detail.value?.startedAt) || detail.value?.status === 'DECLARING' || detail.value?.status === 'COMPLETED')
@@ -67,29 +69,46 @@ function initializeSelection(value: MerchantCustomsContainer) {
}
async function loadDetail() {
if (!props.orderId) return
if (!props.modelValue || !props.orderId) return
const request = ++detailRequest
const orderId = props.orderId
const isCurrent = () => request === detailRequest && props.modelValue && props.orderId === orderId
loading.value = true
errorMessage.value = ''
try {
const result = await getMerchantCustomsForOrder(props.orderId)
const result = await getMerchantCustomsForOrder(orderId)
if (!isCurrent()) return
detail.value = result
initializeSelection(result)
try { clearanceResults.value = await getMerchantClearanceResults(props.orderId) } catch { clearanceResults.value = [] }
let results: MerchantClearanceResult[] = []
try { results = await getMerchantClearanceResults(orderId) } catch { /* Detail remains available for a failed result query. */ }
if (!isCurrent()) return
clearanceResults.value = results
emit('updated', result)
} catch (error) {
if (!isCurrent()) return
detail.value = null
errorMessage.value = merchantCustomsErrorMessage(error, '退税报关资料加载失败')
} finally {
loading.value = false
if (isCurrent()) loading.value = false
}
}
watch(() => props.modelValue, (open) => {
watch(() => [props.modelValue, props.orderId] as const, ([open]) => {
detailRequest += 1
detail.value = null
clearanceResults.value = []
selectedInboundIds.value = []
editingDocument.value = null
replacingDocument.value = null
errorMessage.value = ''
loading.value = false
if (!open) return
detail.value = props.initialDetail ?? null
const initial = props.initialDetail
detail.value = initial?.inbounds.some((item) => item.orderId === props.orderId) ? initial : null
if (detail.value) initializeSelection(detail.value)
void loadDetail()
})
}, { immediate: true })
function close() {
emit('update:modelValue', false)