test: 智能识别与英文名搜索的单测 + e2e
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 6s
PR Preview / test (pull_request) Successful in 6s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Failing after 6m2s
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 6s
PR Preview / test (pull_request) Successful in 6s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Failing after 6m2s
- 将粘贴解析抽到 useOilProductPaste composable - 8 条 vitest 覆盖价格/规格/中英文名/类型判断 - 2 条 cypress 覆盖 UI 填充(产品 100ml、精油 15ml) - 补英文名搜索 e2e;旧 search 用例 placeholder 选择器宽松化 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,12 +16,22 @@ describe('Oil Reference Page', () => {
|
|||||||
it('filters oils by search', () => {
|
it('filters oils by search', () => {
|
||||||
cy.get('.oil-chip').then($chips => {
|
cy.get('.oil-chip').then($chips => {
|
||||||
const initial = $chips.length
|
const initial = $chips.length
|
||||||
cy.get('input[placeholder*="搜索精油"]').type('薰衣草')
|
cy.get('input[placeholder*="搜索"]').type('薰衣草')
|
||||||
cy.wait(300)
|
cy.wait(300)
|
||||||
cy.get('.oil-chip').should('have.length.lt', initial)
|
cy.get('.oil-chip').should('have.length.lt', initial)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('filters oils by english name', () => {
|
||||||
|
cy.get('.oil-chip').then($chips => {
|
||||||
|
const initial = $chips.length
|
||||||
|
cy.get('input[placeholder*="搜索"]').type('Lavender')
|
||||||
|
cy.wait(300)
|
||||||
|
cy.get('.oil-chip').should('have.length.lt', initial)
|
||||||
|
cy.get('.oil-chip').should('exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('toggles between bottle and drop price view', () => {
|
it('toggles between bottle and drop price view', () => {
|
||||||
cy.get('.oil-chip').first().invoke('text').then(textBefore => {
|
cy.get('.oil-chip').first().invoke('text').then(textBefore => {
|
||||||
cy.contains('滴价').click()
|
cy.contains('滴价').click()
|
||||||
|
|||||||
51
frontend/cypress/e2e/oil-smart-paste.cy.js
Normal file
51
frontend/cypress/e2e/oil-smart-paste.cy.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
describe('Oil Reference Smart Paste', () => {
|
||||||
|
let adminToken
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.getAdminToken().then(token => { adminToken = token })
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/oils', {
|
||||||
|
onBeforeLoad(win) {
|
||||||
|
win.localStorage.setItem('oil_auth_token', adminToken)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.get('.oil-chip, .oils-grid', { timeout: 10000 }).should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('smart paste fills product form fields', () => {
|
||||||
|
cy.contains('button', '+ 新增').click()
|
||||||
|
cy.contains('button', '🪄 智能识别').click()
|
||||||
|
|
||||||
|
const sample = [
|
||||||
|
'优惠顾客价:¥310PT:41',
|
||||||
|
'',
|
||||||
|
'零售价:¥465',
|
||||||
|
'',
|
||||||
|
'点数:37 规格:100毫升',
|
||||||
|
'',
|
||||||
|
'花样年华焕颜精华水 Salubelle Rejuvenating Essence',
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
cy.get('textarea').type(sample, { parseSpecialCharSequences: false, delay: 0 })
|
||||||
|
cy.contains('button', '识别并填入').click()
|
||||||
|
|
||||||
|
cy.get('.add-type-tab.active').should('contain', '其他')
|
||||||
|
cy.get('input[placeholder="产品名称"]').should('have.value', '花样年华焕颜精华水')
|
||||||
|
cy.get('input[placeholder="英文名"]').should('have.value', 'Salubelle Rejuvenating Essence')
|
||||||
|
cy.get('input[placeholder="会员价 ¥"]').should('have.value', '310')
|
||||||
|
cy.get('input[placeholder="零售价 ¥"]').should('have.value', '465')
|
||||||
|
cy.get('input[placeholder="容量"]').should('have.value', '100')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('smart paste detects standard ml volume as essential oil', () => {
|
||||||
|
cy.contains('button', '+ 新增').click()
|
||||||
|
cy.contains('button', '🪄 智能识别').click()
|
||||||
|
const sample = '会员价:¥200\n零售价:¥267\n规格:15毫升\n薰衣草测试 LavenderTest'
|
||||||
|
cy.get('textarea').type(sample, { parseSpecialCharSequences: false, delay: 0 })
|
||||||
|
cy.contains('button', '识别并填入').click()
|
||||||
|
cy.get('.add-type-tab.active').should('contain', '精油')
|
||||||
|
cy.get('input[placeholder="精油名称"]').should('have.value', '薰衣草测试')
|
||||||
|
})
|
||||||
|
})
|
||||||
73
frontend/src/__tests__/oilProductPaste.test.js
Normal file
73
frontend/src/__tests__/oilProductPaste.test.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { parseOilProductPaste } from '../composables/useOilProductPaste'
|
||||||
|
|
||||||
|
describe('parseOilProductPaste', () => {
|
||||||
|
it('returns empty shape for empty input', () => {
|
||||||
|
const r = parseOilProductPaste('')
|
||||||
|
expect(r.cn).toBe('')
|
||||||
|
expect(r.en).toBe('')
|
||||||
|
expect(r.memberPrice).toBeNull()
|
||||||
|
expect(r.retailPrice).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses the 花样年华 sample as product with 100ml', () => {
|
||||||
|
const sample = `优惠顾客价:¥310PT:41
|
||||||
|
|
||||||
|
零售价:¥465
|
||||||
|
|
||||||
|
点数:37 规格:100毫升
|
||||||
|
|
||||||
|
花样年华焕颜精华水 Salubelle Rejuvenating Essence`
|
||||||
|
const r = parseOilProductPaste(sample)
|
||||||
|
expect(r.type).toBe('product')
|
||||||
|
expect(r.memberPrice).toBe(310)
|
||||||
|
expect(r.retailPrice).toBe(465)
|
||||||
|
expect(r.productAmount).toBe(100)
|
||||||
|
expect(r.productUnit).toBe('ml')
|
||||||
|
expect(r.cn).toBe('花样年华焕颜精华水')
|
||||||
|
expect(r.en).toBe('Salubelle Rejuvenating Essence')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects essential oil when volume is standard ml', () => {
|
||||||
|
const sample = `会员价:¥200\n零售价:¥267\n规格:15毫升\n薰衣草 Lavender`
|
||||||
|
const r = parseOilProductPaste(sample)
|
||||||
|
expect(r.type).toBe('oil')
|
||||||
|
expect(r.volume).toBe('15')
|
||||||
|
expect(r.cn).toBe('薰衣草')
|
||||||
|
expect(r.en).toBe('Lavender')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles half-width colon and dollar variant', () => {
|
||||||
|
const r = parseOilProductPaste('优惠顾客价: ¥99\n零售价: ¥150\n规格: 5ml\n柠檬 Lemon')
|
||||||
|
expect(r.memberPrice).toBe(99)
|
||||||
|
expect(r.retailPrice).toBe(150)
|
||||||
|
expect(r.type).toBe('oil')
|
||||||
|
expect(r.volume).toBe('5')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses capsule spec as product', () => {
|
||||||
|
const r = parseOilProductPaste('优惠顾客价:¥200\n规格:60粒\n深海鱼油 Omega')
|
||||||
|
expect(r.type).toBe('product')
|
||||||
|
expect(r.productAmount).toBe(60)
|
||||||
|
expect(r.productUnit).toBe('capsule')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses gram spec as product', () => {
|
||||||
|
const r = parseOilProductPaste('优惠顾客价:¥80\n规格:120克\n洁面乳 Face Wash')
|
||||||
|
expect(r.productUnit).toBe('g')
|
||||||
|
expect(r.productAmount).toBe(120)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('non-standard ml volume falls to product', () => {
|
||||||
|
const r = parseOilProductPaste('优惠顾客价:¥310\n规格:100毫升\n精华 Essence')
|
||||||
|
expect(r.type).toBe('product')
|
||||||
|
expect(r.productAmount).toBe(100)
|
||||||
|
expect(r.productUnit).toBe('ml')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('name without english part keeps cn only', () => {
|
||||||
|
const r = parseOilProductPaste('优惠顾客价:¥50\n规格:5毫升\n某国产品')
|
||||||
|
expect(r.cn).toBe('某国产品')
|
||||||
|
expect(r.en).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
52
frontend/src/composables/useOilProductPaste.js
Normal file
52
frontend/src/composables/useOilProductPaste.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const OIL_VOLUMES = new Set(['2.5', '5', '10', '15', '115'])
|
||||||
|
|
||||||
|
export function parseOilProductPaste(raw) {
|
||||||
|
const result = {
|
||||||
|
type: 'product',
|
||||||
|
cn: '',
|
||||||
|
en: '',
|
||||||
|
memberPrice: null,
|
||||||
|
retailPrice: null,
|
||||||
|
volume: null,
|
||||||
|
customDrops: null,
|
||||||
|
productAmount: null,
|
||||||
|
productUnit: null,
|
||||||
|
}
|
||||||
|
if (!raw || !raw.trim()) return result
|
||||||
|
|
||||||
|
const text = raw.replace(/[::]/g, ':').replace(/[¥¥]/g, '')
|
||||||
|
|
||||||
|
const memberMatch = text.match(/(?:优惠顾客价|会员价|批发价)\s*:?\s*(\d+(?:\.\d+)?)/)
|
||||||
|
const retailMatch = text.match(/零售价\s*:?\s*(\d+(?:\.\d+)?)/)
|
||||||
|
const specMatch = text.match(/规格\s*:?\s*(\d+(?:\.\d+)?)\s*(毫升|ml|ML|克|g|G|颗|粒|片)/)
|
||||||
|
|
||||||
|
if (memberMatch) result.memberPrice = Number(memberMatch[1])
|
||||||
|
if (retailMatch) result.retailPrice = Number(retailMatch[1])
|
||||||
|
|
||||||
|
for (const line of raw.split(/\r?\n/)) {
|
||||||
|
const s = line.trim()
|
||||||
|
if (!s) continue
|
||||||
|
if (/优惠顾客价|会员价|零售价|点数|规格|PT\s*:|批发价/i.test(s)) continue
|
||||||
|
const m = s.match(/^([^A-Za-z]+?)\s+([A-Za-z].*)$/)
|
||||||
|
if (m) { result.cn = m[1].trim(); result.en = m[2].trim() } else { result.cn = s }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specMatch) {
|
||||||
|
const amount = specMatch[1]
|
||||||
|
const unitRaw = specMatch[2].toLowerCase()
|
||||||
|
const isMl = unitRaw === '毫升' || unitRaw === 'ml'
|
||||||
|
if (isMl && OIL_VOLUMES.has(String(Number(amount)))) {
|
||||||
|
result.type = 'oil'
|
||||||
|
result.volume = String(Number(amount))
|
||||||
|
} else {
|
||||||
|
result.type = 'product'
|
||||||
|
result.productAmount = Number(amount)
|
||||||
|
result.productUnit = (unitRaw === '克' || unitRaw === 'g') ? 'g'
|
||||||
|
: (unitRaw === '颗' || unitRaw === '粒' || unitRaw === '片') ? 'capsule'
|
||||||
|
: 'ml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -445,6 +445,7 @@ import { oilEn } from '../composables/useOilTranslation'
|
|||||||
import { getOilCard, setOilCard } from '../composables/useOilCards'
|
import { getOilCard, setOilCard } from '../composables/useOilCards'
|
||||||
import { showConfirm } from '../composables/useDialog'
|
import { showConfirm } from '../composables/useDialog'
|
||||||
import { api } from '../composables/useApi'
|
import { api } from '../composables/useApi'
|
||||||
|
import { parseOilProductPaste } from '../composables/useOilProductPaste'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const oils = useOilsStore()
|
const oils = useOilsStore()
|
||||||
@@ -478,51 +479,23 @@ const activeCard = ref(null)
|
|||||||
const addType = ref('oil')
|
const addType = ref('oil')
|
||||||
const showSmartPaste = ref(false)
|
const showSmartPaste = ref(false)
|
||||||
const smartPasteText = ref('')
|
const smartPasteText = ref('')
|
||||||
const OIL_VOLUMES = new Set(['2.5', '5', '10', '15', '115'])
|
|
||||||
|
|
||||||
function runSmartPaste() {
|
function runSmartPaste() {
|
||||||
const raw = smartPasteText.value || ''
|
const raw = smartPasteText.value || ''
|
||||||
if (!raw.trim()) return
|
if (!raw.trim()) return
|
||||||
const text = raw.replace(/[::]/g, ':').replace(/[¥¥]/g, '')
|
const parsed = parseOilProductPaste(raw)
|
||||||
|
if (parsed.memberPrice != null) newBottlePrice.value = parsed.memberPrice
|
||||||
const memberMatch = text.match(/(?:优惠顾客价|会员价|批发价)\s*:?\s*(\d+(?:\.\d+)?)/)
|
if (parsed.retailPrice != null) newRetailPrice.value = parsed.retailPrice
|
||||||
const retailMatch = text.match(/零售价\s*:?\s*(\d+(?:\.\d+)?)/)
|
if (parsed.cn) newOilName.value = parsed.cn
|
||||||
const specMatch = text.match(/规格\s*:?\s*(\d+(?:\.\d+)?)\s*(毫升|ml|ML|克|g|G|颗|粒|片)/)
|
if (parsed.en) newOilEnName.value = parsed.en
|
||||||
|
addType.value = parsed.type
|
||||||
let cn = '', en = ''
|
if (parsed.type === 'oil') {
|
||||||
for (const line of raw.split(/\r?\n/)) {
|
if (parsed.volume) newVolume.value = parsed.volume
|
||||||
const s = line.trim()
|
newCustomDrops.value = null
|
||||||
if (!s) continue
|
|
||||||
if (/优惠顾客价|会员价|零售价|点数|规格|PT\s*:|批发价/i.test(s)) continue
|
|
||||||
const m = s.match(/^([^A-Za-z]+?)\s+([A-Za-z].*)$/)
|
|
||||||
if (m) { cn = m[1].trim(); en = m[2].trim() } else { cn = s }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memberMatch) newBottlePrice.value = Number(memberMatch[1])
|
|
||||||
if (retailMatch) newRetailPrice.value = Number(retailMatch[1])
|
|
||||||
if (cn) newOilName.value = cn
|
|
||||||
if (en) newOilEnName.value = en
|
|
||||||
|
|
||||||
if (specMatch) {
|
|
||||||
const amount = specMatch[1]
|
|
||||||
const unitRaw = specMatch[2].toLowerCase()
|
|
||||||
const isMl = unitRaw === '毫升' || unitRaw === 'ml'
|
|
||||||
if (isMl && OIL_VOLUMES.has(String(Number(amount)))) {
|
|
||||||
addType.value = 'oil'
|
|
||||||
newVolume.value = String(Number(amount))
|
|
||||||
newCustomDrops.value = null
|
|
||||||
} else {
|
|
||||||
addType.value = 'product'
|
|
||||||
newProductAmount.value = Number(amount)
|
|
||||||
newProductUnit.value = (unitRaw === '克' || unitRaw === 'g') ? 'g'
|
|
||||||
: (unitRaw === '颗' || unitRaw === '粒' || unitRaw === '片') ? 'capsule'
|
|
||||||
: 'ml'
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
addType.value = 'product'
|
if (parsed.productAmount != null) newProductAmount.value = parsed.productAmount
|
||||||
|
if (parsed.productUnit) newProductUnit.value = parsed.productUnit
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.showToast('已识别并填入,请检查后点添加')
|
ui.showToast('已识别并填入,请检查后点添加')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user