import { describe, it, expect, beforeEach, vi } from 'vitest' import { mount } from '@vue/test-utils' import { createRouter, createMemoryHistory } from 'vue-router' import { createPinia } from 'pinia' import OrdersPage from '@/pages/OrdersPage.vue' function mockFetchResponse(status: number, body: unknown) { return Promise.resolve({ ok: status >= 200 && status < 300, status, json: () => Promise.resolve(body), }) } function createTestRouter() { return createRouter({ history: createMemoryHistory(), routes: [ { path: '/orders', name: 'orders', component: OrdersPage }, { path: '/betalning/:orderId', name: 'payment', component: { template: '
Payment
' }, }, { path: '/bestallning/:orderId/redigera', name: 'edit-order', component: { template: '
Edit
' }, }, { path: '/', name: 'home', component: { template: '
Home
' } }, ], }) } function mountPage() { const router = createTestRouter() const pinia = createPinia() router.push('/orders') return { router, wrapper: mount(OrdersPage, { global: { plugins: [router, pinia] }, }), } } const mockOrders = [ { id: 'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', plate: 'ABC123', letterText: 'Hej fin bil!', status: 'sent', trackingId: 'PN123456789', createdAt: '2026-05-11T12:00:00Z', }, { id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', plate: 'DEF456', letterText: 'Vill köpa din bil.', status: 'pending_payment', trackingId: null, createdAt: '2026-05-14T13:00:00Z', }, ] function mockOrdersFetch(orders: unknown) { vi.mocked(globalThis.fetch).mockImplementation((url, init) => { const urlStr = String(url) const method = init?.method ?? 'GET' if (urlStr.includes('/payment/swish-info')) { return mockFetchResponse(200, { number: '1234567890', amount: 49 }) } if (urlStr.includes('/cancel') && method === 'POST') { return mockFetchResponse(200, { id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', plate: 'DEF456', letterText: 'Vill köpa din bil.', status: 'cancelled', trackingId: null, createdAt: '2026-05-14T13:00:00Z', }) } if (urlStr.includes('/orders')) { return mockFetchResponse(200, orders) } return mockFetchResponse(404, { message: 'Not found' }) }) } describe('OrdersPage', () => { beforeEach(() => { localStorage.clear() globalThis.fetch = vi.fn() mockOrdersFetch(mockOrders) }) it('renders heading and subtitle', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Mina beställningar') expect(wrapper.text()).toContain( 'Här kan du se dina tidigare beställningar', ) }) it('shows loading state initially', async () => { globalThis.fetch = vi.fn().mockImplementation(() => new Promise(() => {})) const { wrapper } = mountPage() expect(wrapper.text()).toContain('Laddar beställningar...') }) it('shows section headings when pending and completed orders exist', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Obetalda beställningar') expect(wrapper.text()).toContain('Tidigare beställningar') }) it('fetches orders from API on mount', async () => { mountPage() await new Promise((r) => setTimeout(r, 50)) expect(globalThis.fetch).toHaveBeenCalledWith( '/api/orders', expect.objectContaining({ headers: expect.any(Object) }), ) }) it('renders order cards with plate numbers', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('ABC123') expect(wrapper.text()).toContain('DEF456') }) it('renders Swedish status labels', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Skickat') expect(wrapper.text()).toContain('Väntar på betalning') }) it('renders tracking link when trackingId exists', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const link = wrapper.find('a[href*="postnord"]') expect(link.exists()).toBe(true) expect(link.classes()).toContain('orders__tracking-btn') expect(link.text()).toContain('PN123456789') expect(link.text()).toContain('Spåra brev') expect(link.attributes('target')).toBe('_blank') }) it('does not render tracking link when trackingId is null', async () => { const ordersWithoutTracking = [ { id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', plate: 'DEF456', letterText: 'Test', status: 'pending_payment', trackingId: null, createdAt: '2026-05-14T13:00:00Z', }, ] mockOrdersFetch(ordersWithoutTracking) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const link = wrapper.find('a[href*="postnord"]') expect(link.exists()).toBe(false) }) it('renders order id and message', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11') expect(wrapper.text()).toContain('Beställnings-ID') expect(wrapper.text()).toContain('Hej fin bil!') expect(wrapper.text()).toContain('Vill köpa din bil.') }) it('renders formatted date', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('2026') expect(wrapper.text()).toContain('Skapad') }) it('shows empty state when no orders', async () => { mockOrdersFetch([]) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Inga beställningar ännu') }) it('shows error state on API failure', async () => { vi.mocked(globalThis.fetch).mockImplementation((url) => { const urlStr = String(url) if (urlStr.includes('/payment/swish-info')) { return mockFetchResponse(200, { number: '1234567890', amount: 49 }) } return mockFetchResponse(500, { message: 'Internal server error' }) }) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Kunde inte hämta beställningar') }) it('applies correct badge class for status', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const badges = wrapper.findAll('.badge') expect(badges[0].classes()).toContain('badge--warning') expect(badges[1].classes()).toContain('badge--success') }) it('shows order id on pending payment orders', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const pendingCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('DEF456')) expect(pendingCard?.text()).toContain('Beställnings-ID') expect(pendingCard?.text()).toContain( 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', ) }) it('shows pay button only for pending payment orders', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const pendingCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('DEF456')) const payLink = pendingCard?.find('a.orders__pay-btn') expect(payLink?.exists()).toBe(true) expect(payLink?.text()).toBe('Betala 49 kr') const href = payLink?.attributes('href') expect(href).toContain('c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12') expect(href).toContain('plate=DEF456') }) it('does not show pay button for paid or sent orders', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const sentCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('ABC123')) expect(sentCard?.find('a.orders__pay-btn').exists()).toBe(false) }) it('shows edit link for pending payment orders', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const pendingCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('DEF456')) const editLink = pendingCard?.find('a.orders__edit-btn') expect(editLink?.exists()).toBe(true) expect(editLink?.text()).toBe('Redigera brev') const href = editLink?.attributes('href') expect(href).toContain('c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12') expect(href).toContain('redigera') }) it('shows cancel button for pending payment orders', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const pendingCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('DEF456')) const cancelBtn = pendingCard?.find('.orders__cancel-btn') expect(cancelBtn?.exists()).toBe(true) expect(cancelBtn?.text()).toBe('Avbryt beställning') }) it('calls cancel API and updates status to Avbruten', async () => { vi.stubGlobal( 'confirm', vi.fn(() => true), ) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const pendingCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('DEF456')) await pendingCard?.find('.orders__cancel-btn').trigger('click') await new Promise((r) => setTimeout(r, 50)) expect(globalThis.fetch).toHaveBeenCalledWith( '/api/orders/c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12/cancel', expect.objectContaining({ method: 'POST' }), ) expect(wrapper.text()).toContain('Avbruten') vi.unstubAllGlobals() }) it('does not show edit or cancel actions for non-pending orders', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const sentCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('ABC123')) expect(sentCard?.find('.orders__cancel-btn').exists()).toBe(false) expect(sentCard?.text()).not.toContain('Redigera brev') expect(sentCard?.text()).not.toContain('Avbryt beställning') }) it('renders processing status correctly', async () => { const ordersWithProcessing = [ { id: 'c4eebc99-9c0b-4ef8-bb6d-6bb9bd380a14', plate: 'XYZ123', letterText: 'Processing message', status: 'processing', trackingId: null, createdAt: '2026-05-15T10:00:00Z', }, ] mockOrdersFetch(ordersWithProcessing) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Hanteras') const badge = wrapper.find('.badge') expect(badge.classes()).toContain('badge--primary') }) it('shows expand toggle for long messages and reveals full text', async () => { const longText = 'Hej! Jag ville nämna en situation i trafiken där vi båda kanske blev lite frustrerade. Det är lätt att det blir så när man kör bil i rusningstid och tempot blir högt.' const ordersWithLongMessage = [ { id: 'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', plate: 'ABC123', letterText: longText, status: 'processing', trackingId: null, createdAt: '2026-05-11T12:00:00Z', }, ] mockOrdersFetch(ordersWithLongMessage) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const card = wrapper.find('.orders__card') const preview = card.find('.orders__preview') const toggle = card.find('.orders__preview-toggle') expect(toggle.exists()).toBe(true) expect(toggle.text()).toBe('Visa mer') expect(preview.classes()).not.toContain('orders__preview--expanded') await toggle.trigger('click') expect(preview.classes()).toContain('orders__preview--expanded') expect(toggle.text()).toBe('Visa mindre') expect(card.text()).toContain(longText) }) it('does not show expand toggle for short messages', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.find('.orders__preview-toggle').exists()).toBe(false) }) })