describe('Responsive Design', () => { describe('Mobile viewport (375x667)', () => { beforeEach(() => { cy.viewport(375, 667) }) it('loads the app on mobile', () => { cy.visit('/') cy.get('.app-header').should('be.visible') cy.contains('doTERRA').should('be.visible') }) it('nav tabs are scrollable', () => { cy.visit('/') cy.get('.nav-tabs').should('have.css', 'overflow-x', 'auto') }) it('recipe cards stack in single column', () => { cy.visit('/') cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) // On mobile, cards should be full width cy.get('.recipe-card').first().then($card => { const width = $card.outerWidth() expect(width).to.be.gte(300) }) }) it('search input is usable on mobile', () => { cy.visit('/') cy.get('input[placeholder*="搜索"]').should('be.visible') cy.get('input[placeholder*="搜索"]').type('薰衣草') cy.get('input[placeholder*="搜索"]').should('have.value', '薰衣草') }) it('oil reference page works on mobile', () => { cy.visit('/oils') cy.contains('精油价目').should('be.visible') cy.get('.oil-chip').should('have.length.gte', 1) }) }) 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) }) it('loads and shows recipe grid', () => { cy.visit('/') cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) }) it('oil grid shows multiple columns', () => { cy.visit('/oils') cy.get('.oil-chip', { timeout: 10000 }).should('have.length.gte', 1) }) }) describe('Wide viewport (1920x1080)', () => { beforeEach(() => { cy.viewport(1920, 1080) }) it('content is centered with max-width', () => { cy.visit('/') cy.get('.main').then($main => { const width = $main.outerWidth() expect(width).to.be.lte(960) }) }) it('recipe grid shows multiple columns', () => { cy.visit('/') cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) }) }) })