feat(web): remember batch purchase device per user (#233)
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: 414e33d4e0db8a71925a93fcb6b733cf175c0a95
|
||||
synchronized_at: 2026-09-07T02:51:26Z
|
||||
wiki_revision: 670a5592a6db8301cd115295bf820f7f4b6e06d7
|
||||
synchronized_at: 2026-09-07T03:21:47Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -438,3 +438,9 @@ synchronized_at: 2026-09-07T02:51:26Z
|
||||
- 直接启动失败(包括无可处理 Activity)时回退既有浏览器入口;启动请求被接受后仍由执行器验证稳定商品页面,停留首页不会仅因启动成功而判定完成。
|
||||
- 清栈不用于改地址后返回、下单后回到 PDD、订单核查;采集入口继续使用既有浏览器流程,避免绕过规则中的浏览器步骤。
|
||||
- 当前通用商品页面结构验证不能独立证明页面 goods_id;解析结果携带的任务 goodsId 不是页面回读证据。首次直接深链上线仍需以实际目标商品真机核对,ADB 实验不替代 Agent 上下文验证。
|
||||
## SYB 批量采购设备偏好(#233)
|
||||
|
||||
- SYB 商品页“批量创建采购任务”按当前登录用户 ID、当前浏览器来源与 API 环境记住最近一次设备选择;主动清空也记忆为不指定设备。只保存设备 ID,不保存 Token、账号凭据或订单信息。
|
||||
- 弹窗打开后先按现有在线/可选/采购能力条件加载设备,再恢复选择并以相同设备预检。记忆设备当前不可用时保留偏好并提示用户重新选择或明确清空,不静默切换设备或自动领取。
|
||||
- 预检加载中、失败或记忆设备不可用时不能提交;过期预检结果不覆盖新的设备选择。服务端原有设备及采购资格校验不变。
|
||||
- 偏好只作用于此入口,不影响采集、其他创建入口和采购重试。浏览器存储失败时仍允许手动操作;刷新或关闭再打开浏览器可恢复,清理浏览器数据或沿用现有退出登录清理存储行为后需重新选择。
|
||||
|
||||
@@ -5,6 +5,7 @@ const getters = {
|
||||
visitedViews: state => state.tagsView.visitedViews,
|
||||
cachedViews: state => state.tagsView.cachedViews,
|
||||
token: state => state.user.token,
|
||||
userId: state => state.user.userId,
|
||||
avatar: state => state.user.avatar,
|
||||
name: state => state.user.name,
|
||||
introduction: state => state.user.introduction,
|
||||
|
||||
@@ -5,6 +5,7 @@ import storage from '@/utils/storage'
|
||||
|
||||
const state = {
|
||||
token: getToken(),
|
||||
userId: null,
|
||||
name: '',
|
||||
avatar: '',
|
||||
introduction: '',
|
||||
@@ -16,6 +17,10 @@ const state = {
|
||||
const mutations = {
|
||||
SET_TOKEN: (state, token) => {
|
||||
state.token = token
|
||||
if (!token) state.userId = null
|
||||
},
|
||||
SET_USER_ID: (state, userId) => {
|
||||
state.userId = Number(userId) || null
|
||||
},
|
||||
SET_INTRODUCTION: (state, introduction) => {
|
||||
state.introduction = introduction
|
||||
@@ -63,13 +68,14 @@ const actions = {
|
||||
return resolve()
|
||||
}
|
||||
|
||||
const { roles, name, avatar, introduction, permissions } = response.data
|
||||
const { roles, name, avatar, introduction, permissions, userId } = response.data
|
||||
|
||||
// roles must be a non-empty array
|
||||
if (!roles || roles.length <= 0) {
|
||||
return reject('getInfo: roles must be a non-null array!')
|
||||
}
|
||||
commit('SET_PERMISSIONS', permissions)
|
||||
commit('SET_USER_ID', userId)
|
||||
commit('SET_ROLES', roles)
|
||||
commit('SET_NAME', name)
|
||||
commit('SET_AVATAR', avatar)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
function preferenceKey(userId) {
|
||||
const id = Number(userId)
|
||||
if (!Number.isSafeInteger(id) || id <= 0) return null
|
||||
return `goauto:purchase-device:${encodeURIComponent(process.env.VUE_APP_BASE_API || '/')}:user:${id}`
|
||||
}
|
||||
|
||||
export function readPurchaseDevice(userId) {
|
||||
try {
|
||||
const key = preferenceKey(userId)
|
||||
if (!key) return null
|
||||
const id = JSON.parse(localStorage.getItem(key) || 'null')
|
||||
return Number.isSafeInteger(id) && id > 0 ? id : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function rememberPurchaseDevice(userId, deviceId) {
|
||||
try {
|
||||
const key = preferenceKey(userId)
|
||||
if (!key) return false
|
||||
const id = deviceId == null || deviceId === '' ? null : Number(deviceId)
|
||||
if (id !== null && (!Number.isSafeInteger(id) || id <= 0)) return false
|
||||
localStorage.setItem(key, JSON.stringify(id))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,16 @@
|
||||
<el-alert title="每条 SYB 商品分别创建一个采购任务和一个待付款 PDD 订单;系统永不支付。创建前服务端会再次逐条检查。" type="warning" :closable="false" show-icon class="notice" />
|
||||
<div class="purchase-summary" aria-live="polite"><span>已选择 <strong>{{ purchaseDialog.selectedCount }}</strong> 条 SYB 明细</span><span class="success-text">其中 {{ purchaseDialog.eligibleCount }} 条适用于采购</span><span v-if="purchaseDialog.skippedCount" class="danger-text">预检后另有 {{ purchaseDialog.skippedCount }} 条不可创建</span></div>
|
||||
<el-form label-width="110px" class="purchase-settings">
|
||||
<el-form-item label="Android 设备"><el-select v-model="purchaseDialog.deviceId" clearable placeholder="不指定,由空闲设备领取" style="width:360px" @change="refreshPurchasePreview"><el-option v-for="device in purchaseDevices" :key="device.id" :label="`${device.name} · ${device.model}`" :value="device.id" /></el-select><span class="field-help">默认人工指定;留空时由符合能力的空闲设备领取。</span></el-form-item>
|
||||
<el-form-item label="Android 设备">
|
||||
<el-select v-model="purchaseDialog.deviceId" clearable placeholder="不指定,由空闲设备领取" style="width:360px" :disabled="purchaseDialog.saving" @change="changePurchaseDevice">
|
||||
<el-option v-if="purchaseDeviceUnavailable" :value="purchaseDialog.deviceId" label="上次选择的设备(当前不可用)" disabled />
|
||||
<el-option v-for="device in purchaseDevices" :key="device.id" :label="`${device.name} · ${device.model}`" :value="device.id" />
|
||||
</el-select>
|
||||
<span v-if="purchaseDeviceUnavailable" class="field-help danger-text" role="alert">上次选择的设备当前不可用,请重新选择,或清空后由空闲设备领取。</span>
|
||||
<span v-else class="field-help">记住当前用户在此浏览器的选择;留空时由符合能力的空闲设备领取。</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-alert v-if="purchaseDialog.previewError" :title="purchaseDialog.previewError" type="error" :closable="false" show-icon class="notice"><el-button link type="primary" @click="openPurchaseBatch">重试</el-button></el-alert>
|
||||
<el-table :data="purchaseDialog.items" border size="small" max-height="380" empty-text="没有可创建的商品">
|
||||
<el-table-column label="SYB 商品" min-width="150"><template #default="{ row }"><strong>{{ row.orderCode || `ID ${row.sybProductId}` }}</strong><div class="muted">{{ row.shopeeItemId || '未关联蝦皮商品' }}</div></template></el-table-column>
|
||||
<el-table-column label="商品" min-width="210"><template #default="{ row }"><div class="ellipsis">{{ row.productTitle || '—' }}</div><div class="muted">PDD {{ row.pddGoodsId || '未关联' }}</div></template></el-table-column>
|
||||
@@ -47,7 +55,7 @@
|
||||
</el-table>
|
||||
<p class="muted">如果某条商品在确认后状态发生变化,只跳过该条,不撤销其他成功任务。</p>
|
||||
</div>
|
||||
<template #footer><el-button :disabled="purchaseDialog.saving" @click="purchaseDialog.open = false">返回列表</el-button><el-button type="primary" :loading="purchaseDialog.saving" :disabled="purchaseDialog.loading || purchaseDialog.eligibleCount === 0" @click="submitPurchaseBatch">创建 {{ purchaseDialog.eligibleCount }} 个采购任务</el-button></template>
|
||||
<template #footer><el-button :disabled="purchaseDialog.saving" @click="purchaseDialog.open = false">返回列表</el-button><el-button type="primary" :loading="purchaseDialog.saving" :disabled="purchaseDialog.loading || purchaseDeviceUnavailable || !!purchaseDialog.previewError || purchaseDialog.eligibleCount === 0" @click="submitPurchaseBatch">创建 {{ purchaseDialog.eligibleCount }} 个采购任务</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量创建采购任务结果 -->
|
||||
@@ -171,6 +179,7 @@ import { createPurchaseTasksBatch, matchPurchaseSpecsBatch, previewPurchaseTasks
|
||||
import { batchCreateCollectionTasks } from '@/api/goauto/collection-tasks'
|
||||
import { listCollectionRules } from '@/api/goauto/collection-rules'
|
||||
import { createRequestId } from '@/utils/request-id'
|
||||
import { readPurchaseDevice, rememberPurchaseDevice } from '@/utils/purchase-device-preference'
|
||||
import ShopeeProductDetailDrawer from '../shopee-products/ShopeeProductDetailDrawer.vue'
|
||||
import PddProductDetailDrawer from '../pdd-products/PddProductDetailDrawer.vue'
|
||||
|
||||
@@ -183,8 +192,9 @@ export default {
|
||||
loading: false, products: [], selectedProducts: [], total: 0,
|
||||
purchaseReadiness: {}, purchaseReadinessLoading: false, specMatchLoading: false, loadGeneration: 0,
|
||||
purchaseDevices: [],
|
||||
purchasePreviewGeneration: 0,
|
||||
shopOptions: [], shopOptionsLoaded: false, shopOptionsPromise: null,
|
||||
purchaseDialog: { open: false, loading: false, saving: false, ids: [], selectedCount: 0, deviceId: null, items: [], eligibleCount: 0, skippedCount: 0 },
|
||||
purchaseDialog: { open: false, loading: false, saving: false, ids: [], selectedCount: 0, deviceId: null, items: [], eligibleCount: 0, skippedCount: 0, previewError: '' },
|
||||
purchaseResult: { open: false, items: [], createdCount: 0, failedCount: 0 },
|
||||
specMatchResult: { open: false, items: [], selectedIDs: [], autoConfirmedCount: 0, pendingCount: 0, failedCount: 0, skippedCount: 0 },
|
||||
collectionBatch: this.emptyCollectionBatch(),
|
||||
@@ -198,6 +208,7 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
purchaseDeviceUnavailable() { return !!this.purchaseDialog.deviceId && !this.purchaseDevices.some(device => device.id === this.purchaseDialog.deviceId) },
|
||||
canPurchase() { const roles = this.$store.getters.roles || []; return roles.includes('admin') || roles.includes('purchaser') },
|
||||
processStageOptions() { return [{ value: 'manual_action', label: '待人工处理' }, { value: 'pdd_unlinked', label: '未关联 PDD' }, { value: 'pdd_pending', label: 'PDD 待采集' }, { value: 'pdd_collecting', label: 'PDD 采集中' }, { value: 'pdd_collection_failed', label: 'PDD 采集失败' }, { value: 'color_mapping', label: '规格待匹配' }, { value: 'purchase_ready', label: '可创建采购' }, { value: 'task_created', label: '已创建任务' }, { value: 'purchase_succeeded', label: '采购成功' }, { value: 'order_review', label: '待人工核对' }] },
|
||||
aiMatchCandidates() { return this.selectedProducts.filter(row => this.purchaseReady(row).aiMatchEligible === true) },
|
||||
@@ -353,23 +364,46 @@ export default {
|
||||
const candidates = this.purchaseCandidates
|
||||
if (!candidates.length) return
|
||||
const ids = candidates.map(item => item.id)
|
||||
this.purchaseDialog = { open: true, loading: true, saving: false, ids, selectedCount: this.selectedProducts.length, deviceId: null, items: [], eligibleCount: 0, skippedCount: 0 }
|
||||
this.purchaseDevices = []
|
||||
this.purchaseDialog = { open: true, loading: true, saving: false, ids, selectedCount: this.selectedProducts.length, deviceId: readPurchaseDevice(this.$store.getters.userId), items: [], eligibleCount: 0, skippedCount: 0, previewError: '' }
|
||||
const dialog = this.purchaseDialog
|
||||
try {
|
||||
const [preview, devices] = await Promise.all([
|
||||
previewPurchaseTasks({ sybProductIds: ids }),
|
||||
listDevices({ page: 1, pageSize: 100, status: 'online' })
|
||||
])
|
||||
this.applyPurchasePreview(preview.data)
|
||||
const devices = await listDevices({ page: 1, pageSize: 100, status: 'online' })
|
||||
if (this.purchaseDialog !== dialog || !dialog.open) return
|
||||
const required = ['purchase.live.v1', 'purchase.address-update.v1', 'purchase.order-create.v1', 'purchase.spec-probe.v1']
|
||||
this.purchaseDevices = devices.data.items.filter(device => device.selectable && required.every(capability => (device.capabilities || []).includes(capability)))
|
||||
} finally { this.purchaseDialog.loading = false }
|
||||
await this.refreshPurchasePreview()
|
||||
} catch {
|
||||
if (this.purchaseDialog === dialog) {
|
||||
dialog.previewError = '设备列表加载失败,请重试。'
|
||||
dialog.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
applyPurchasePreview(data) { this.purchaseDialog.items = data.items; this.purchaseDialog.eligibleCount = data.eligibleCount; this.purchaseDialog.skippedCount = data.skippedCount },
|
||||
changePurchaseDevice() {
|
||||
if (!rememberPurchaseDevice(this.$store.getters.userId, this.purchaseDialog.deviceId)) ElMessage.warning('本次选择已生效,但未能保存设备偏好;下次可能需要重新选择。')
|
||||
return this.refreshPurchasePreview()
|
||||
},
|
||||
async refreshPurchasePreview() {
|
||||
this.purchaseDialog.loading = true
|
||||
try { const r = await previewPurchaseTasks({ sybProductIds: this.purchaseDialog.ids, deviceId: this.purchaseDialog.deviceId || undefined }); this.applyPurchasePreview(r.data) } finally { this.purchaseDialog.loading = false }
|
||||
const dialog = this.purchaseDialog
|
||||
const generation = ++this.purchasePreviewGeneration
|
||||
dialog.eligibleCount = 0
|
||||
dialog.items = []
|
||||
dialog.previewError = ''
|
||||
if (this.purchaseDeviceUnavailable) { dialog.loading = false; return }
|
||||
dialog.loading = true
|
||||
try {
|
||||
const r = await previewPurchaseTasks({ sybProductIds: dialog.ids, deviceId: dialog.deviceId || undefined })
|
||||
if (this.purchaseDialog === dialog && generation === this.purchasePreviewGeneration) this.applyPurchasePreview(r.data)
|
||||
} catch {
|
||||
if (this.purchaseDialog === dialog && generation === this.purchasePreviewGeneration) dialog.previewError = '采购预检失败,请重试。'
|
||||
} finally {
|
||||
if (this.purchaseDialog === dialog && generation === this.purchasePreviewGeneration) dialog.loading = false
|
||||
}
|
||||
},
|
||||
async submitPurchaseBatch() {
|
||||
if (this.purchaseDialog.loading || this.purchaseDialog.saving || this.purchaseDeviceUnavailable || this.purchaseDialog.previewError || !this.purchaseDialog.eligibleCount) return
|
||||
this.purchaseDialog.saving = true
|
||||
try {
|
||||
const r = await createPurchaseTasksBatch({ requestId: createRequestId(), sybProductIds: this.purchaseDialog.ids, deviceId: this.purchaseDialog.deviceId || undefined })
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const required = ['purchase.live.v1', 'purchase.address-update.v1', 'purchase.order-create.v1', 'purchase.spec-probe.v1']
|
||||
const routes = [{ path: '/workbench', component: 'Layout', menuName: 'Workbench', title: '工作台', visible: '0', children: [
|
||||
{ path: '/syb-products', component: '/goauto/syb-products/index', menuName: 'GoAutoSybProducts', title: 'SYB 商品', visible: '0' }
|
||||
] }]
|
||||
|
||||
async function mock(page: any) {
|
||||
const state = { userId: 1, available: true, previewFailed: false, previews: [] as any[], created: [] as any[] }
|
||||
await page.context().addCookies([{ name: 'Admin-Token', value: 'mock-token', domain: 'localhost', path: '/' }])
|
||||
await page.route('**/api/**', async (route: any) => {
|
||||
const url = new URL(route.request().url())
|
||||
const ok = (data: any) => route.fulfill({ json: { code: 200, data } })
|
||||
if (url.pathname.startsWith('/src/api/')) return route.continue()
|
||||
if (url.pathname.endsWith('/api/v1/getinfo')) return ok({ userId: state.userId, roles: ['admin'], name: '同名测试用户', avatar: '', introduction: '', permissions: [] })
|
||||
if (url.pathname.endsWith('/api/v1/menurole')) return ok(routes)
|
||||
if (url.pathname.endsWith('/api/admin/v1/syb-products')) return ok({ items: [
|
||||
{ id: 1, orderCode: 'TEST-1', shopeeItemId: '1001', shopeeProductId: 2, productTitle: '测试商品', targetColor: '黑色', targetSize: 'XL', quantity: 1, parseStatus: 'success' }
|
||||
], total: 1, page: 1, pageSize: 20 })
|
||||
if (url.pathname.endsWith('/api/admin/v1/devices')) return ok({ items: [
|
||||
{ id: 10, name: '测试设备A', model: 'A', selectable: state.available, capabilities: required },
|
||||
{ id: 20, name: '测试设备B', model: 'B', selectable: true, capabilities: required }
|
||||
], total: 2 })
|
||||
if (url.pathname.endsWith('/purchase-tasks/batch-preview')) {
|
||||
const body = route.request().postDataJSON()
|
||||
state.previews.push(body)
|
||||
if (state.previewFailed) return route.fulfill({ status: 500, json: { code: 500, message: '模拟预检失败' } })
|
||||
return ok({ items: [{ sybProductId: 1, eligible: true, processStage: 'purchase_ready', reason: '规格就绪', quantity: 1 }], eligibleCount: 1, skippedCount: 0 })
|
||||
}
|
||||
if (url.pathname.endsWith('/purchase-tasks/batch')) {
|
||||
state.created.push(route.request().postDataJSON())
|
||||
return ok({ items: [], createdCount: 1, failedCount: 0 })
|
||||
}
|
||||
return ok([])
|
||||
})
|
||||
return state
|
||||
}
|
||||
|
||||
async function open(page: any, reload = false) {
|
||||
if (reload) await page.reload()
|
||||
else await page.goto('/#/syb-products')
|
||||
await expect(page.locator('tbody').getByText('可创建采购', { exact: true })).toBeVisible()
|
||||
await page.locator('.el-table__body-wrapper tbody tr').first().locator('.el-checkbox').click()
|
||||
await page.getByRole('button', { name: /^创建采购\s*1$/ }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '批量创建采购任务' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.locator('.el-loading-mask')).toBeHidden()
|
||||
return dialog
|
||||
}
|
||||
|
||||
async function choose(page: any, dialog: any, name: string) {
|
||||
await dialog.locator('.el-select').click()
|
||||
await page.getByRole('option', { name, exact: true }).click()
|
||||
await expect(dialog.locator('.el-loading-mask')).toBeHidden()
|
||||
}
|
||||
|
||||
test('设备选择跨弹窗和刷新恢复,预检与创建使用同一设备', async ({ page }) => {
|
||||
const state = await mock(page)
|
||||
let dialog = await open(page)
|
||||
await choose(page, dialog, '测试设备A · A')
|
||||
await dialog.getByRole('button', { name: '返回列表' }).click()
|
||||
await page.getByRole('button', { name: /^创建采购\s*1$/ }).click()
|
||||
await expect(dialog.getByText('测试设备A · A', { exact: true })).toBeVisible()
|
||||
await expect.poll(() => state.previews.at(-1)?.deviceId).toBe(10)
|
||||
dialog = await open(page, true)
|
||||
await expect(dialog.getByText('测试设备A · A', { exact: true })).toBeVisible()
|
||||
await dialog.getByRole('button', { name: '创建 1 个采购任务' }).click()
|
||||
await expect.poll(() => state.created.length).toBe(1)
|
||||
expect(state.created[0].deviceId).toBe(10)
|
||||
})
|
||||
|
||||
test('不可用设备不被静默替换,主动清空后保持不指定', async ({ page }) => {
|
||||
const state = await mock(page)
|
||||
let dialog = await open(page)
|
||||
await choose(page, dialog, '测试设备A · A')
|
||||
state.available = false
|
||||
dialog = await open(page, true)
|
||||
await expect(dialog.getByRole('alert').filter({ hasText: '上次选择的设备当前不可用' })).toBeVisible()
|
||||
await expect(dialog.getByRole('button', { name: /创建 .* 个采购任务/ })).toBeDisabled()
|
||||
await dialog.locator('.el-select').hover()
|
||||
await dialog.locator('.el-select__clear').click()
|
||||
await expect.poll(() => state.previews.at(-1)?.deviceId).toBeUndefined()
|
||||
dialog = await open(page, true)
|
||||
await expect(dialog.getByText('上次选择的设备(当前不可用)', { exact: true })).toHaveCount(0)
|
||||
await expect(dialog.getByText('不指定,由空闲设备领取', { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test('同名用户偏好按ID隔离,预检失败禁用创建', async ({ page }) => {
|
||||
const state = await mock(page)
|
||||
let dialog = await open(page)
|
||||
await choose(page, dialog, '测试设备A · A')
|
||||
state.userId = 2
|
||||
dialog = await open(page, true)
|
||||
await expect(dialog.getByText('不指定,由空闲设备领取', { exact: true })).toBeVisible()
|
||||
await choose(page, dialog, '测试设备B · B')
|
||||
state.userId = 1
|
||||
dialog = await open(page, true)
|
||||
await expect(dialog.getByText('测试设备A · A', { exact: true })).toBeVisible()
|
||||
state.previewFailed = true
|
||||
await choose(page, dialog, '测试设备B · B')
|
||||
await expect(dialog.getByText('采购预检失败,请重试。', { exact: true })).toBeVisible()
|
||||
await expect(dialog.getByRole('button', { name: /创建 .* 个采购任务/ })).toBeDisabled()
|
||||
expect(state.created).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('关闭重开后旧预检响应不覆盖新设备结果', async ({ page }) => {
|
||||
await mock(page)
|
||||
const dialog = await open(page)
|
||||
let held = false
|
||||
let finished = false
|
||||
let release: () => void = () => {}
|
||||
const gate = new Promise<void>(resolve => { release = resolve })
|
||||
await page.route('**/purchase-tasks/batch-preview', async route => {
|
||||
if (route.request().postDataJSON().deviceId !== 10 || held) return route.fallback()
|
||||
held = true
|
||||
await gate
|
||||
await route.fulfill({ json: { code: 200, data: { items: [], eligibleCount: 0, skippedCount: 1 } } })
|
||||
finished = true
|
||||
})
|
||||
await dialog.locator('.el-select').click()
|
||||
await page.getByRole('option', { name: '测试设备A · A', exact: true }).click()
|
||||
await expect.poll(() => held).toBe(true)
|
||||
await expect(dialog.getByRole('button', { name: /创建 .* 个采购任务/ })).toBeDisabled()
|
||||
await dialog.getByRole('button', { name: '返回列表' }).click()
|
||||
await page.getByRole('button', { name: /^创建采购\s*1$/ }).click()
|
||||
await expect(dialog.locator('.el-loading-mask')).toBeHidden()
|
||||
await choose(page, dialog, '测试设备B · B')
|
||||
release()
|
||||
await expect.poll(() => finished).toBe(true)
|
||||
await expect(dialog.getByText('测试设备B · B', { exact: true })).toBeVisible()
|
||||
await expect(dialog.getByRole('button', { name: '创建 1 个采购任务' })).toBeEnabled()
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { readPurchaseDevice, rememberPurchaseDevice } from '@/utils/purchase-device-preference'
|
||||
|
||||
describe('purchase device preference', () => {
|
||||
beforeEach(() => { localStorage.clear() })
|
||||
afterEach(() => { jest.restoreAllMocks() })
|
||||
|
||||
it('isolates users and remembers an explicit empty selection', () => {
|
||||
expect(rememberPurchaseDevice(1, 10)).toBe(true)
|
||||
expect(rememberPurchaseDevice(2, 20)).toBe(true)
|
||||
expect(readPurchaseDevice(1)).toBe(10)
|
||||
expect(readPurchaseDevice(2)).toBe(20)
|
||||
expect(rememberPurchaseDevice(1, '')).toBe(true)
|
||||
expect(readPurchaseDevice(1)).toBeNull()
|
||||
expect(readPurchaseDevice(2)).toBe(20)
|
||||
})
|
||||
|
||||
it('does not share a fallback key when user identity is missing', () => {
|
||||
expect(rememberPurchaseDevice(null, 10)).toBe(false)
|
||||
expect(readPurchaseDevice(null)).toBeNull()
|
||||
expect(localStorage.length).toBe(0)
|
||||
})
|
||||
|
||||
it('ignores corrupt preferences and tolerates unavailable browser storage', () => {
|
||||
rememberPurchaseDevice(1, 10)
|
||||
localStorage.setItem(localStorage.key(0), '{broken')
|
||||
expect(readPurchaseDevice(1)).toBeNull()
|
||||
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('disabled') })
|
||||
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('disabled') })
|
||||
expect(readPurchaseDevice(1)).toBeNull()
|
||||
expect(rememberPurchaseDevice(1, 10)).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user