更新
This commit is contained in:
@@ -152,3 +152,211 @@ test('batch save retains partial failures and never reports queued work as remot
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(state.operationResults.value[1].sync_error, /企微请求超时/)
|
||||
})
|
||||
|
||||
test('direct batch sync snapshots and deduplicates selection, limits concurrency, and waits for refresh', async () => {
|
||||
const pools = [pool(1), pool(2), pool(3), pool(4)]
|
||||
const pending = []
|
||||
const unrelatedCalls = []
|
||||
let active = 0
|
||||
let maximum = 0
|
||||
let refreshCount = 0
|
||||
let finishRefresh
|
||||
const api = Object.fromEntries([
|
||||
'wecomPromotionBatchUpdatePools', 'wecomPromotionSavePool', 'wecomPromotionSaveMember',
|
||||
'wecomPromotionBatchSetOperators', 'wecomPromotionSyncCustomers', 'wecomPromotionToggleMember'
|
||||
].map((name) => [name, async (payload) => { unrelatedCalls.push({ name, payload }); return {} }]))
|
||||
const { state, messages } = page({
|
||||
...api,
|
||||
wecomPromotionOverview: () => {
|
||||
refreshCount++
|
||||
return new Promise((resolve) => { finishRefresh = () => resolve({ pools }) })
|
||||
},
|
||||
wecomPromotionSyncMemberRange: ({ pool_id }) => new Promise((resolve) => {
|
||||
active++
|
||||
maximum = Math.max(maximum, active)
|
||||
pending.push({ id: pool_id, finish: () => {
|
||||
active--
|
||||
resolve({ pool_id, sync_status: 'synced', sync_error: '', sync_queued: false })
|
||||
} })
|
||||
})
|
||||
}, pools)
|
||||
state.selectedPoolIds.value = [1, 1, 2, 3]
|
||||
let completed = false
|
||||
const syncing = state.syncSelectedMemberRanges().then(() => { completed = true })
|
||||
assert.equal(state.syncingBatchMembers.value, true)
|
||||
assert.deepEqual(pending.map((item) => item.id), [1, 2])
|
||||
assert.deepEqual({ ...state.batchSyncProgress }, { completed: 0, total: 3 })
|
||||
|
||||
// A changed ref must not alter the already-dispatched batch's work list.
|
||||
state.selectedPoolIds.value = [4]
|
||||
pending[0].finish()
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.deepEqual(pending.map((item) => item.id), [1, 2, 3])
|
||||
assert.equal(state.batchSyncProgress.completed, 1)
|
||||
assert.equal(refreshCount, 0)
|
||||
pending[1].finish()
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(state.batchSyncProgress.completed, 2)
|
||||
assert.equal(completed, false)
|
||||
assert.equal(refreshCount, 0)
|
||||
pending[2].finish()
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(maximum, 2)
|
||||
assert.equal(refreshCount, 1)
|
||||
assert.equal(state.syncingBatchMembers.value, true)
|
||||
assert.equal(state.batchSyncProgress.completed, 3)
|
||||
assert.equal(completed, false)
|
||||
finishRefresh()
|
||||
await syncing
|
||||
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
assert.deepEqual({ ...state.batchSyncProgress }, { completed: 0, total: 0 })
|
||||
assert.deepEqual(state.selectedPoolIds.value, [])
|
||||
assert.deepEqual(state.operationResults.value.map((item) => item.id), [1, 2, 3])
|
||||
assert.ok(state.operationResults.value.every((item) => item.sync_only && item.sync_status === 'synced'))
|
||||
assert.deepEqual(unrelatedCalls, [])
|
||||
assert.equal(messages.at(-1).type, 'success')
|
||||
assert.match(messages.at(-1).message, /已确认同步 3 个/)
|
||||
assert.doesNotMatch(messages.at(-1).message, /本地.*保存/)
|
||||
for (const result of state.operationResults.value) {
|
||||
assert.match(state.operationResultText(result), /已确认同步/)
|
||||
assert.doesNotMatch(state.operationResultText(result), /本地.*保存/)
|
||||
}
|
||||
})
|
||||
|
||||
test('direct batch sync keeps failed, pending and blocked pools selected with precise results', async () => {
|
||||
const pools = [pool(1), pool(2), pool(3), { ...pool(4), official_link: null }, { ...pool(5), can_operate: false }]
|
||||
const calls = []
|
||||
let refreshCount = 0
|
||||
const { state, messages } = page({
|
||||
wecomPromotionOverview: async () => { refreshCount++; return { pools } },
|
||||
wecomPromotionSyncMemberRange: async ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
if (pool_id === 2) throw new Error('企微请求超时,请检查网络')
|
||||
return { pool_id, sync_status: pool_id === 1 ? 'synced' : 'pending', sync_queued: pool_id === 3, sync_error: '' }
|
||||
}
|
||||
}, pools)
|
||||
state.selectedPoolIds.value = [1, 2, 3, 4, 5]
|
||||
await state.syncSelectedMemberRanges()
|
||||
|
||||
assert.deepEqual(calls, [1, 2, 3])
|
||||
assert.equal(refreshCount, 1)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [2, 3, 4, 5])
|
||||
assert.deepEqual(state.operationResults.value.map((item) => item.sync_status), ['synced', 'failed', 'pending', 'blocked', 'blocked'])
|
||||
assert.ok(state.operationResults.value.every((item) => item.sync_only))
|
||||
assert.match(state.operationResults.value[1].sync_error, /企微请求超时,请检查网络/)
|
||||
assert.equal(state.operationResults.value[2].sync_queued, true)
|
||||
assert.match(state.operationResultText(state.operationResults.value[3]), /链接/)
|
||||
assert.match(state.operationResultText(state.operationResults.value[4]), /权限|无权/)
|
||||
for (const result of state.operationResults.value) assert.doesNotMatch(state.operationResultText(result), /本地.*保存/)
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(messages.at(-1).message, /已确认同步 1 个/)
|
||||
assert.match(messages.at(-1).message, /4 个尚未确认同步/)
|
||||
assert.doesNotMatch(messages.at(-1).message, /本地.*保存/)
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
assert.deepEqual({ ...state.batchSyncProgress }, { completed: 0, total: 0 })
|
||||
})
|
||||
|
||||
test('direct batch sync prevents duplicate requests and freezes member and selection changes while busy', async () => {
|
||||
const pools = [pool(1), pool(2)]
|
||||
const calls = []
|
||||
const toggles = []
|
||||
let finish
|
||||
const { state } = page({
|
||||
wecomPromotionSyncMemberRange: ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
return new Promise((resolve) => { finish = () => resolve({ pool_id, sync_status: 'synced' }) })
|
||||
},
|
||||
wecomPromotionToggleMember: async (payload) => { toggles.push(payload); return { sync_status: 'synced' } }
|
||||
}, pools)
|
||||
state.selectedPoolIds.value = [1]
|
||||
const syncing = state.syncSelectedMemberRanges()
|
||||
await state.syncSelectedMemberRanges()
|
||||
await state.syncMemberRange(2)
|
||||
await state.handleMemberToggle(member(2), false)
|
||||
state.togglePoolSelection(pools[0], false)
|
||||
state.togglePoolSelection(pools[1], true)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
state.toggleAllPoolSelection(false)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
state.toggleAllPoolSelection(true)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
assert.deepEqual(calls, [1])
|
||||
assert.deepEqual(toggles, [])
|
||||
finish()
|
||||
await syncing
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
})
|
||||
|
||||
test('direct batch sync cannot begin while a single sync, member toggle or batch save is active', async () => {
|
||||
const calls = []
|
||||
const { state } = page({ wecomPromotionSyncMemberRange: async (payload) => { calls.push(payload); return { sync_status: 'synced' } } })
|
||||
state.selectedPoolIds.value = [1]
|
||||
for (const [busy, value] of [[state.syncingPoolId, 1], [state.togglingMemberId, 2], [state.savingBatchConfig, true]]) {
|
||||
busy.value = value
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.deepEqual(calls, [])
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
busy.value = typeof value === 'boolean' ? false : 0
|
||||
}
|
||||
})
|
||||
|
||||
test('direct batch sync rejects empty and oversized selections but accepts 100 unique pools', async () => {
|
||||
const pools = Array.from({ length: 101 }, (_, index) => pool(index + 1))
|
||||
const calls = []
|
||||
const { state, messages } = page({ wecomPromotionSyncMemberRange: async ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
return { pool_id, sync_status: 'synced' }
|
||||
} }, pools)
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(messages.at(-1).message, /选择|勾选/)
|
||||
assert.deepEqual(calls, [])
|
||||
state.selectedPoolIds.value = pools.map((item) => item.id)
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(messages.at(-1).message, /100/)
|
||||
assert.deepEqual(calls, [])
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
state.selectedPoolIds.value = [...pools.slice(0, 100).map((item) => item.id), 1]
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.deepEqual(calls, pools.slice(0, 100).map((item) => item.id))
|
||||
assert.equal(messages.at(-1).type, 'success')
|
||||
})
|
||||
|
||||
test('shared operators can select and sync pools while access and configuration stay manager-only', async () => {
|
||||
const pools = [
|
||||
{ ...pool(1), can_manage_access: false },
|
||||
{ ...pool(2), can_operate: false },
|
||||
{ ...pool(3), can_operate: false, can_manage_access: false },
|
||||
pool(4)
|
||||
]
|
||||
const calls = []
|
||||
const { state, messages } = page({ wecomPromotionSyncMemberRange: async ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
return { pool_id, sync_status: 'synced' }
|
||||
} }, pools)
|
||||
assert.deepEqual(state.selectablePoolIds.value, [1, 2, 4])
|
||||
state.togglePoolSelection(pools[0], true)
|
||||
state.togglePoolSelection(pools[2], true)
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
state.openAccessDialog()
|
||||
assert.equal(state.accessDialogVisible.value, false)
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
state.openBatchConfigDialog()
|
||||
assert.equal(state.batchConfigDialogVisible.value, false)
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.deepEqual(calls, [1])
|
||||
|
||||
state.toggleAllPoolSelection(true)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1, 2, 4])
|
||||
state.openAccessDialog()
|
||||
assert.deepEqual(state.accessForm.pool_ids, [2, 4])
|
||||
state.openBatchConfigDialog()
|
||||
assert.deepEqual(state.batchConfigForm.pool_ids, [2, 4])
|
||||
state.overview.pools[0].can_operate = false
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(state.selectedPoolIds.value, [2, 4])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user