- Truncate previews over 120 characters with a Visa mer toggle - Allow per-order expand state on pending and completed cards - Add styles for expanded preview and toggle button - Cover long and short message behavior in OrdersPage tests Co-authored-by: Cursor <cursoragent@cursor.com>
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
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: '<div>Payment</div>' },
|
|
},
|
|
{
|
|
path: '/bestallning/:orderId/redigera',
|
|
name: 'edit-order',
|
|
component: { template: '<div>Edit</div>' },
|
|
},
|
|
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
|
],
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|