fix: 手机编辑配方弹层合适宽度 + 打开时屏蔽 tab 滑动 #44
@@ -62,35 +62,48 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
cd frontend
|
||||||
CYPRESS_CFG="video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,requestTimeout=5000,responseTimeout=10000,baseUrl=http://localhost:$FE_PORT,experimentalMemoryManagement=true,numTestsKeptInMemory=0"
|
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 ==="
|
run_batch() {
|
||||||
timeout 300 npx cypress run \
|
local name="$1"; shift
|
||||||
--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" \
|
echo "=== $name ==="
|
||||||
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
|
timeout 300 npx cypress run \
|
||||||
B1=$?
|
--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 ==="
|
run_batch "Batch 1a: API parity" \
|
||||||
timeout 300 npx cypress run \
|
"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"
|
||||||
--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" \
|
run_batch "Batch 1b: features" \
|
||||||
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
|
"cypress/e2e/registration-flow.cy.js,cypress/e2e/pr27-features.cy.js,cypress/e2e/kit-export.cy.js"
|
||||||
B2=$?
|
|
||||||
|
|
||||||
echo "=== Batch 3: Remaining tests ==="
|
run_batch "Batch 2a: auth & nav" \
|
||||||
timeout 300 npx cypress run \
|
"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"
|
||||||
--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" \
|
run_batch "Batch 2b: user flows" \
|
||||||
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
|
"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"
|
||||||
B3=$?
|
|
||||||
|
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
|
# Cleanup
|
||||||
kill $BE_PID $FE_PID 2>/dev/null
|
kill $BE_PID $FE_PID 2>/dev/null
|
||||||
pkill -f "Cypress" 2>/dev/null || true
|
pkill -f "Cypress" 2>/dev/null || true
|
||||||
rm -f "$DB_FILE"
|
rm -f "$DB_FILE"
|
||||||
|
|
||||||
echo "Results: Batch1=$B1 Batch2=$B2 Batch3=$B3"
|
if [ $FAIL -ne 0 ]; then
|
||||||
if [ $B1 -ne 0 ] || [ $B2 -ne 0 ] || [ $B3 -ne 0 ]; then
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -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)', () => {
|
describe('Tablet viewport (768x1024)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.viewport(768, 1024)
|
cy.viewport(768, 1024)
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ function onSwipeEnd() {
|
|||||||
// Carousel area excluded
|
// Carousel area excluded
|
||||||
if (swipeStartTarget.value?.closest?.('[data-no-tab-swipe]')) return
|
if (swipeStartTarget.value?.closest?.('[data-no-tab-swipe]')) return
|
||||||
// Skip when modal/overlay is open
|
// 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 tabs = visibleTabs.value.map(t => t.key)
|
||||||
const currentIdx = tabs.indexOf(ui.currentSection)
|
const currentIdx = tabs.indexOf(ui.currentSection)
|
||||||
|
|||||||
@@ -2307,6 +2307,9 @@ watch(() => recipeStore.recipes, () => {
|
|||||||
|
|
||||||
.form-select {
|
.form-select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1.5px solid #d4cfc7;
|
border: 1.5px solid #d4cfc7;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -2328,6 +2331,7 @@ watch(() => recipeStore.recipes, () => {
|
|||||||
|
|
||||||
.oil-search-wrap {
|
.oil-search-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2483,5 +2487,22 @@ watch(() => recipeStore.recipes, () => {
|
|||||||
.manage-toolbar {
|
.manage-toolbar {
|
||||||
flex-direction: column;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user