describe('Kit Export Feature', () => { let adminToken let authHeaders before(() => { cy.getAdminToken().then(token => { adminToken = token authHeaders = { Authorization: `Bearer ${token}` } }) }) describe('API: recipes and oils available', () => { it('loads oils list', () => { cy.request({ url: '/api/oils', headers: authHeaders }).then(res => { expect(res.status).to.eq(200) expect(res.body).to.be.an('array') expect(res.body.length).to.be.greaterThan(50) }) }) it('loads recipes list', () => { cy.request({ url: '/api/recipes', headers: authHeaders }).then(res => { expect(res.status).to.eq(200) expect(res.body).to.be.an('array') expect(res.body.length).to.be.greaterThan(10) }) }) }) describe('UI: Projects page access', () => { it('shows login prompt when not logged in', () => { cy.visit('/projects') cy.contains('登录', { timeout: 10000 }).should('be.visible') }) it('shows kit compare button when logged in as admin', () => { cy.visit('/projects', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', adminToken) } }) cy.contains('套装方案对比', { timeout: 10000 }).should('be.visible') }) }) describe('UI: Kit Export page', () => { beforeEach(() => { cy.visit('/kit-export', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', adminToken) } }) }) it('loads with 4 kit cards', () => { cy.get('.kit-card', { timeout: 10000 }).should('have.length', 4) }) it('shows kit names and prices', () => { cy.contains('芳香调理套装').should('be.visible') cy.contains('家庭医生套装').should('be.visible') cy.contains('居家呵护套装').should('be.visible') cy.contains('全精油套装').should('be.visible') cy.contains('¥1575').should('be.visible') cy.contains('¥2250').should('be.visible') cy.contains('¥3988').should('be.visible') cy.contains('¥17700').should('be.visible') }) it('shows recipe count for each kit', () => { cy.get('.kit-recipe-count').should('have.length', 4) cy.get('.kit-recipe-count').each($el => { expect($el.text()).to.match(/可做 \d+ 个配方/) }) }) it('clicking a kit card shows its recipes', () => { cy.get('.kit-card').eq(1).click() cy.get('.kit-detail', { timeout: 5000 }).should('be.visible') cy.get('.recipe-table').should('exist') }) it('recipe table has cost and profit columns', () => { cy.get('.kit-card').first().click() cy.get('.recipe-table', { timeout: 5000 }).within(() => { cy.contains('th', '套装成本').should('exist') cy.contains('th', '单买成本').should('exist') cy.contains('th', '售价').should('exist') cy.contains('th', '利润率').should('exist') cy.contains('th', '可做次数').should('exist') }) }) it('shows cross comparison section', () => { cy.get('.cross-section', { timeout: 10000 }).should('be.visible') cy.get('.cross-table').should('exist') }) it('cross comparison has single-buy column', () => { cy.get('.cross-table', { timeout: 10000 }).within(() => { cy.contains('th', '单买').should('exist') }) }) it('cross comparison shows staircase pattern (available cells have green bg)', () => { cy.get('.td-kit-available', { timeout: 10000 }).should('have.length.greaterThan', 0) cy.get('.td-kit-na').should('have.length.greaterThan', 0) }) it('kit cost is always <= original cost in recipe table', () => { cy.get('.kit-card').first().click() cy.get('.recipe-table tbody tr', { timeout: 5000 }).each($row => { const cells = $row.find('td') // td[1] = 可做次数, td[2] = 套装成本, td[3] = 单买成本 const kitCostText = cells.eq(2).text().replace(/[¥,\s]/g, '') const origCostText = cells.eq(3).text().replace(/[¥,\s]/g, '') const kitCost = parseFloat(kitCostText) const origCost = parseFloat(origCostText) if (!isNaN(kitCost) && !isNaN(origCost)) { expect(kitCost).to.be.at.most(origCost + 0.01) } }) }) it('export buttons exist', () => { cy.contains('button', '导出完整版').should('be.visible') cy.contains('button', '导出简版').should('be.visible') }) it('shows volume info next to recipe name', () => { cy.get('.td-volume', { timeout: 10000 }).should('have.length.greaterThan', 0) }) }) describe('UI: Kit Export access control', () => { it('redirects to /projects when not logged in', () => { cy.visit('/kit-export') cy.url({ timeout: 10000 }).should('include', '/projects') }) }) })