diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 11eb7cf..316fd41 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -62,35 +62,48 @@ jobs: exit 1 fi - # Run all specs in 3 batches to avoid Electron memory crashes + # Run specs in smaller batches (≤6 per run) to avoid Electron memory + # hangs seen when a single cypress run handles 10+ specs on the CI runner. cd frontend CYPRESS_CFG="video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,requestTimeout=5000,responseTimeout=10000,baseUrl=http://localhost:$FE_PORT,experimentalMemoryManagement=true,numTestsKeptInMemory=0" + FAIL=0 - echo "=== Batch 1: API & data tests ===" - timeout 300 npx cypress run \ - --spec "cypress/e2e/api-crud.cy.js,cypress/e2e/api-health.cy.js,cypress/e2e/oil-data-integrity.cy.js,cypress/e2e/recipe-cost-parity.cy.js,cypress/e2e/endpoint-parity.cy.js,cypress/e2e/registration-flow.cy.js,cypress/e2e/pr27-features.cy.js,cypress/e2e/kit-export.cy.js" \ - --config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN" - B1=$? + run_batch() { + local name="$1"; shift + echo "=== $name ===" + timeout 300 npx cypress run \ + --spec "$1" \ + --config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN" + local rc=$? + if [ $rc -ne 0 ]; then + echo "!!! $name failed (rc=$rc)" + FAIL=1 + fi + } - echo "=== Batch 2: UI flow tests ===" - timeout 300 npx cypress run \ - --spec "cypress/e2e/auth-flow.cy.js,cypress/e2e/admin-flow.cy.js,cypress/e2e/navigation.cy.js,cypress/e2e/recipe-detail.cy.js,cypress/e2e/recipe-search.cy.js,cypress/e2e/manage-recipes.cy.js,cypress/e2e/diary-flow.cy.js,cypress/e2e/favorites.cy.js,cypress/e2e/inventory-flow.cy.js,cypress/e2e/demo-walkthrough.cy.js" \ - --config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN" - B2=$? + run_batch "Batch 1a: API parity" \ + "cypress/e2e/api-crud.cy.js,cypress/e2e/api-health.cy.js,cypress/e2e/oil-data-integrity.cy.js,cypress/e2e/recipe-cost-parity.cy.js,cypress/e2e/endpoint-parity.cy.js" + run_batch "Batch 1b: features" \ + "cypress/e2e/registration-flow.cy.js,cypress/e2e/pr27-features.cy.js,cypress/e2e/kit-export.cy.js" - echo "=== Batch 3: Remaining tests ===" - timeout 300 npx cypress run \ - --spec "cypress/e2e/app-load.cy.js,cypress/e2e/account-settings.cy.js,cypress/e2e/audit-log-advanced.cy.js,cypress/e2e/batch-operations.cy.js,cypress/e2e/bug-tracker-flow.cy.js,cypress/e2e/category-modules.cy.js,cypress/e2e/notification-flow.cy.js,cypress/e2e/oil-reference.cy.js,cypress/e2e/oil-smart-paste.cy.js,cypress/e2e/performance.cy.js,cypress/e2e/price-display.cy.js,cypress/e2e/projects-flow.cy.js,cypress/e2e/responsive.cy.js,cypress/e2e/search-advanced.cy.js,cypress/e2e/user-management-flow.cy.js,cypress/e2e/visual-check.cy.js" \ - --config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN" - B3=$? + run_batch "Batch 2a: auth & nav" \ + "cypress/e2e/auth-flow.cy.js,cypress/e2e/admin-flow.cy.js,cypress/e2e/navigation.cy.js,cypress/e2e/recipe-detail.cy.js,cypress/e2e/recipe-search.cy.js" + run_batch "Batch 2b: user flows" \ + "cypress/e2e/manage-recipes.cy.js,cypress/e2e/diary-flow.cy.js,cypress/e2e/favorites.cy.js,cypress/e2e/inventory-flow.cy.js,cypress/e2e/demo-walkthrough.cy.js" + + run_batch "Batch 3a: misc flows" \ + "cypress/e2e/app-load.cy.js,cypress/e2e/account-settings.cy.js,cypress/e2e/audit-log-advanced.cy.js,cypress/e2e/batch-operations.cy.js,cypress/e2e/bug-tracker-flow.cy.js,cypress/e2e/category-modules.cy.js" + run_batch "Batch 3b: pages & perf" \ + "cypress/e2e/notification-flow.cy.js,cypress/e2e/oil-reference.cy.js,cypress/e2e/oil-smart-paste.cy.js,cypress/e2e/performance.cy.js,cypress/e2e/price-display.cy.js,cypress/e2e/projects-flow.cy.js" + run_batch "Batch 3c: responsive & admin" \ + "cypress/e2e/responsive.cy.js,cypress/e2e/search-advanced.cy.js,cypress/e2e/user-management-flow.cy.js,cypress/e2e/visual-check.cy.js" # Cleanup kill $BE_PID $FE_PID 2>/dev/null pkill -f "Cypress" 2>/dev/null || true rm -f "$DB_FILE" - echo "Results: Batch1=$B1 Batch2=$B2 Batch3=$B3" - if [ $B1 -ne 0 ] || [ $B2 -ne 0 ] || [ $B3 -ne 0 ]; then + if [ $FAIL -ne 0 ]; then exit 1 fi diff --git a/frontend/cypress/e2e/responsive.cy.js b/frontend/cypress/e2e/responsive.cy.js index 8cc5689..04efa25 100644 --- a/frontend/cypress/e2e/responsive.cy.js +++ b/frontend/cypress/e2e/responsive.cy.js @@ -39,6 +39,75 @@ describe('Responsive Design', () => { }) }) + describe('Mobile edit recipe overlay (375x667)', () => { + let adminToken + + before(() => { + cy.getAdminToken().then(token => { adminToken = token }) + }) + + beforeEach(() => { + cy.viewport(375, 667) + cy.visit('/manage', { + onBeforeLoad(win) { + win.localStorage.setItem('oil_auth_token', adminToken) + } + }) + cy.get('.recipe-manager', { timeout: 10000 }).should('exist') + cy.contains('公共配方库', { timeout: 10000 }).should('be.visible').click() + cy.get('.recipe-row', { timeout: 10000 }).should('have.length.gte', 1) + cy.get('.recipe-row .row-info').first().click() + cy.get('.overlay-panel', { timeout: 5000 }).should('be.visible') + }) + + it('editor table fits within panel without horizontal overflow', () => { + cy.get('.overlay-panel').then($panel => { + const panel = $panel[0] + expect(panel.scrollWidth, 'panel has no horizontal overflow') + .to.be.lte(panel.clientWidth + 1) + }) + cy.get('.overlay-panel .editor-table').then($table => { + const tableRect = $table[0].getBoundingClientRect() + const panelRect = Cypress.$('.overlay-panel')[0].getBoundingClientRect() + expect(tableRect.right, 'table right edge').to.be.lte(panelRect.right + 1) + expect(tableRect.left, 'table left edge').to.be.gte(panelRect.left - 1) + }) + }) + + it('all 4 data column headers (成分/用量/单价/小计) are visible in panel', () => { + const headers = ['成分', '用量', '单价', '小计'] + headers.forEach(label => { + cy.get('.overlay-panel .editor-table thead th').contains(label).then($th => { + const thRect = $th[0].getBoundingClientRect() + const panelRect = Cypress.$('.overlay-panel')[0].getBoundingClientRect() + expect(thRect.right, `${label} header right`).to.be.lte(panelRect.right + 1) + expect(thRect.left, `${label} header left`).to.be.gte(panelRect.left - 1) + }) + }) + }) + + it('oil search input does not push the row past panel edge', () => { + cy.get('.overlay-panel .form-select').first().then($input => { + const inputRect = $input[0].getBoundingClientRect() + const panelRect = Cypress.$('.overlay-panel')[0].getBoundingClientRect() + expect(inputRect.right, 'oil input right').to.be.lte(panelRect.right + 1) + }) + }) + + it('horizontal swipe does not switch tabs while editor overlay is open', () => { + cy.get('.nav-tab.active').invoke('text').then(activeBefore => { + // Overlay covers .main — touch events bubble from .overlay up to .main's handler + cy.get('.overlay') + .trigger('touchstart', { touches: [{ clientX: 320, clientY: 400 }], force: true }) + .trigger('touchmove', { touches: [{ clientX: 60, clientY: 400 }], force: true }) + .trigger('touchend', { force: true }) + cy.wait(200) + cy.get('.overlay-panel').should('be.visible') + cy.get('.nav-tab.active').invoke('text').should('eq', activeBefore) + }) + }) + }) + describe('Tablet viewport (768x1024)', () => { beforeEach(() => { cy.viewport(768, 1024) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b766d69..4c3cf58 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -196,7 +196,7 @@ function onSwipeEnd() { // Carousel area excluded if (swipeStartTarget.value?.closest?.('[data-no-tab-swipe]')) return // Skip when modal/overlay is open - if (document.querySelector('.modal-overlay, .detail-overlay, .dialog-overlay')) return + if (document.querySelector('.modal-overlay, .detail-overlay, .dialog-overlay, .overlay')) return const tabs = visibleTabs.value.map(t => t.key) const currentIdx = tabs.indexOf(ui.currentSection) diff --git a/frontend/src/views/RecipeManager.vue b/frontend/src/views/RecipeManager.vue index 042ce8b..1c7921c 100644 --- a/frontend/src/views/RecipeManager.vue +++ b/frontend/src/views/RecipeManager.vue @@ -2307,6 +2307,9 @@ watch(() => recipeStore.recipes, () => { .form-select { flex: 1; + width: 100%; + min-width: 0; + box-sizing: border-box; padding: 8px 10px; border: 1.5px solid #d4cfc7; border-radius: 8px; @@ -2328,6 +2331,7 @@ watch(() => recipeStore.recipes, () => { .oil-search-wrap { flex: 1; + width: 100%; position: relative; } @@ -2483,5 +2487,22 @@ watch(() => recipeStore.recipes, () => { .manage-toolbar { flex-direction: column; } + .overlay-panel { + padding: 16px; + border-radius: 12px; + max-height: calc(100vh - 32px); + } + .overlay-header { + margin-bottom: 12px; + } + .overlay-header h3 { + font-size: 15px; + } + .ratio-hint { + white-space: normal; + } + .editor-section { + margin-bottom: 12px; + } }