diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index 49dfb19..43a4911 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -58,9 +58,9 @@ jobs:
exit 1
fi
- # Run core cypress specs with timeouts
+ # Run core cypress specs with hard 3-minute timeout
cd frontend
- npx cypress run --spec "\
+ timeout 180 npx cypress run --spec "\
cypress/e2e/recipe-detail.cy.js,\
cypress/e2e/oil-reference.cy.js,\
cypress/e2e/oil-data-integrity.cy.js,\
@@ -68,12 +68,17 @@ jobs:
cypress/e2e/category-modules.cy.js,\
cypress/e2e/notification-flow.cy.js,\
cypress/e2e/registration-flow.cy.js\
- " --config "video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,baseUrl=http://localhost:$FE_PORT"
+ " --config "video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,requestTimeout=5000,responseTimeout=10000,baseUrl=http://localhost:$FE_PORT"
EXIT_CODE=$?
# Cleanup
kill $BE_PID $FE_PID 2>/dev/null
+ pkill -f "Cypress" 2>/dev/null || true
rm -f "$DB_FILE"
+ if [ $EXIT_CODE -eq 124 ]; then
+ echo "ERROR: Cypress timed out after 3 minutes"
+ exit 1
+ fi
exit $EXIT_CODE
build-check:
diff --git a/frontend/src/__tests__/newFeatures.test.js b/frontend/src/__tests__/newFeatures.test.js
index 0150bae..8b50b8c 100644
--- a/frontend/src/__tests__/newFeatures.test.js
+++ b/frontend/src/__tests__/newFeatures.test.js
@@ -85,3 +85,51 @@ describe('EDITOR_ONLY_TAGS', () => {
expect(EDITOR_ONLY_TAGS).toContain('已审核')
})
})
+
+// ---------------------------------------------------------------------------
+// English search
+// ---------------------------------------------------------------------------
+describe('English search matching', () => {
+ const { oilEn } = require('../composables/useOilTranslation')
+
+ it('oilEn returns English name for known oils', () => {
+ expect(oilEn('薰衣草')).toBe('Lavender')
+ expect(oilEn('茶树')).toBe('Tea Tree')
+ expect(oilEn('乳香')).toBe('Frankincense')
+ })
+
+ it('oilEn returns empty for unknown oils', () => {
+ expect(oilEn('不存在的油')).toBeFalsy()
+ })
+
+ it('English query detection', () => {
+ const isEn = (q) => /^[a-zA-Z\s]+$/.test(q)
+ expect(isEn('lavender')).toBe(true)
+ expect(isEn('Tea Tree')).toBe(true)
+ expect(isEn('薰衣草')).toBe(false)
+ expect(isEn('lav3')).toBe(false)
+ })
+
+ it('English matches oil name in recipe', () => {
+ const recipe = {
+ name: '助眠配方',
+ en_name: 'Sleep Aid Blend',
+ ingredients: [{ oil: '薰衣草', drops: 3 }],
+ tags: []
+ }
+ const q = 'lavender'
+ const isEn = /^[a-zA-Z\s]+$/.test(q)
+ const enNameMatch = isEn && (recipe.en_name || '').toLowerCase().includes(q)
+ const oilEnMatch = isEn && recipe.ingredients.some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(q))
+ expect(oilEnMatch).toBe(true)
+ expect(enNameMatch).toBe(false)
+ })
+
+ it('English matches recipe en_name', () => {
+ const recipe = { name: '助眠', en_name: 'Sleep Aid Blend', ingredients: [], tags: [] }
+ const q = 'sleep'
+ const isEn = /^[a-zA-Z\s]+$/.test(q)
+ const enNameMatch = isEn && (recipe.en_name || '').toLowerCase().includes(q)
+ expect(enNameMatch).toBe(true)
+ })
+})
diff --git a/frontend/src/views/RecipeSearch.vue b/frontend/src/views/RecipeSearch.vue
index 6f142f2..1cac3b2 100644
--- a/frontend/src/views/RecipeSearch.vue
+++ b/frontend/src/views/RecipeSearch.vue
@@ -175,6 +175,7 @@ import { useUiStore } from '../stores/ui'
import { api } from '../composables/useApi'
import RecipeCard from '../components/RecipeCard.vue'
import RecipeDetailOverlay from '../components/RecipeDetailOverlay.vue'
+import { oilEn } from '../composables/useOilTranslation'
const auth = useAuthStore()
const oils = useOilsStore()
@@ -313,11 +314,14 @@ function expandQuery(q) {
const exactResults = computed(() => {
if (!searchQuery.value.trim()) return []
const q = searchQuery.value.trim().toLowerCase()
+ const isEn = /^[a-zA-Z\s]+$/.test(q)
return recipeStore.recipes.filter(r => {
const nameMatch = r.name.toLowerCase().includes(q)
+ const enNameMatch = isEn && (r.en_name || '').toLowerCase().includes(q)
+ const oilEnMatch = isEn && r.ingredients.some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(q))
const visibleTags = auth.canEdit ? (r.tags || []) : (r.tags || []).filter(t => !EDITOR_ONLY_TAGS.includes(t))
const tagMatch = visibleTags.some(t => t.toLowerCase().includes(q))
- return nameMatch || tagMatch
+ return nameMatch || enNameMatch || oilEnMatch || tagMatch
}).sort((a, b) => a.name.localeCompare(b.name, 'zh'))
})
diff --git a/frontend/src/views/UserManagement.vue b/frontend/src/views/UserManagement.vue
index 52de97a..1676f63 100644
--- a/frontend/src/views/UserManagement.vue
+++ b/frontend/src/views/UserManagement.vue
@@ -31,6 +31,7 @@
{{ group.latest.display_name || group.latest.username }}
商户名:{{ group.latest.business_name }}
{{ { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[group.effectiveStatus] }}
+