feat(guest): guest checkout without login (Swish + QR) #17
4 changed files with 787 additions and 0 deletions
185
frontend/src/__tests__/GuestCheckoutPage.spec.ts
Normal file
185
frontend/src/__tests__/GuestCheckoutPage.spec.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockCreateGuestOrder: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/guestOrders', () => ({
|
||||
createGuestOrder: mocks.mockCreateGuestOrder,
|
||||
}))
|
||||
|
||||
import GuestCheckoutPage from '@/pages/GuestCheckoutPage.vue'
|
||||
import { createGuestOrder } from '@/api/guestOrders'
|
||||
const mockCreateGuestOrder = vi.mocked(createGuestOrder)
|
||||
|
||||
function createTestRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
||||
{
|
||||
path: '/gast-kassa',
|
||||
name: 'guest-checkout',
|
||||
component: GuestCheckoutPage,
|
||||
},
|
||||
{
|
||||
path: '/gast-betalning/:orderId',
|
||||
name: 'guest-payment',
|
||||
component: { template: '<div>Payment</div>' },
|
||||
},
|
||||
{
|
||||
path: '/logga-in',
|
||||
name: 'login',
|
||||
component: { template: '<div>Login</div>' },
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async function mountPage() {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const router = createTestRouter()
|
||||
await router.push({ name: 'guest-checkout' })
|
||||
await router.isReady()
|
||||
|
||||
const wrapper = mount(GuestCheckoutPage, {
|
||||
global: { plugins: [router, pinia] },
|
||||
})
|
||||
|
||||
return { wrapper, router }
|
||||
}
|
||||
|
||||
describe('GuestCheckoutPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders heading and price', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
expect(wrapper.text()).toContain('Skicka ett brev')
|
||||
expect(wrapper.text()).toContain('49 kr')
|
||||
expect(wrapper.text()).toContain('Inget konto behövs')
|
||||
})
|
||||
|
||||
it('disables submit button when fields are empty', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
const submit = wrapper.find('.guest-checkout__submit')
|
||||
expect(submit.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('enables submit button when plate, letter, and email are valid', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
const inputs = wrapper.findAll('input')
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
await inputs[0].setValue('ABC123')
|
||||
await textarea.setValue('Hej, din bil står fel!')
|
||||
await inputs[1].setValue('test@example.se')
|
||||
|
||||
const submit = wrapper.find('.guest-checkout__submit')
|
||||
expect(submit.attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps submit disabled with an invalid plate', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
const inputs = wrapper.findAll('input')
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
await inputs[0].setValue('ABC') // too short
|
||||
await textarea.setValue('Hej')
|
||||
await inputs[1].setValue('test@example.se')
|
||||
|
||||
const submit = wrapper.find('.guest-checkout__submit')
|
||||
expect(submit.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('keeps submit disabled with an invalid email', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
const inputs = wrapper.findAll('input')
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
await inputs[0].setValue('ABC123')
|
||||
await textarea.setValue('Hej')
|
||||
await inputs[1].setValue('not-an-email')
|
||||
|
||||
const submit = wrapper.find('.guest-checkout__submit')
|
||||
expect(submit.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('shows character count that updates with input', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
await textarea.setValue('Hej')
|
||||
|
||||
expect(wrapper.text()).toContain('3 / 1000 tecken')
|
||||
})
|
||||
|
||||
it('shows link to login page', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
expect(wrapper.text()).toContain('Har du redan ett konto?')
|
||||
expect(wrapper.text()).toContain('Logga in')
|
||||
})
|
||||
|
||||
it('calls createGuestOrder and navigates on submit', async () => {
|
||||
mockCreateGuestOrder.mockResolvedValue({
|
||||
id: 'order-123',
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej',
|
||||
status: 'pending_payment',
|
||||
trackingId: null,
|
||||
amountPaid: null,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
guestToken: 'token-abc',
|
||||
})
|
||||
|
||||
const { wrapper, router } = await mountPage()
|
||||
const inputs = wrapper.findAll('input')
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
await inputs[0].setValue('ABC123')
|
||||
await textarea.setValue('Hej, din bil står fel!')
|
||||
await inputs[1].setValue('test@example.se')
|
||||
|
||||
await wrapper.find('form').trigger('submit.prevent')
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockCreateGuestOrder).toHaveBeenCalledWith(
|
||||
'ABC123',
|
||||
'Hej, din bil står fel!',
|
||||
'test@example.se',
|
||||
)
|
||||
})
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(router.currentRoute.value.name).toBe('guest-payment')
|
||||
expect(router.currentRoute.value.params.orderId).toBe('order-123')
|
||||
expect(router.currentRoute.value.query.token).toBe('token-abc')
|
||||
expect(router.currentRoute.value.query.plate).toBe('ABC123')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error message when order creation fails', async () => {
|
||||
mockCreateGuestOrder.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { wrapper } = await mountPage()
|
||||
const inputs = wrapper.findAll('input')
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
await inputs[0].setValue('ABC123')
|
||||
await textarea.setValue('Hej')
|
||||
await inputs[1].setValue('test@example.se')
|
||||
|
||||
await wrapper.find('form').trigger('submit.prevent')
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Kunde inte skapa beställningen. Försök igen senare.',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
191
frontend/src/__tests__/GuestOrderPage.spec.ts
Normal file
191
frontend/src/__tests__/GuestOrderPage.spec.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockFetchGuestOrder: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/guestOrders', () => ({
|
||||
fetchGuestOrder: mocks.mockFetchGuestOrder,
|
||||
}))
|
||||
|
||||
import GuestOrderPage from '@/pages/GuestOrderPage.vue'
|
||||
|
||||
const mockOrder = {
|
||||
id: 'order-123',
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej, din bil står fel!',
|
||||
status: 'pending_payment',
|
||||
trackingId: null,
|
||||
amountPaid: null,
|
||||
createdAt: '2025-01-01T12:00:00Z',
|
||||
guestToken: 'token-abc',
|
||||
}
|
||||
|
||||
function createTestRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
||||
{
|
||||
path: '/gast-order/:token',
|
||||
name: 'guest-order',
|
||||
component: GuestOrderPage,
|
||||
},
|
||||
{
|
||||
path: '/gast-betalning/:orderId',
|
||||
name: 'guest-payment',
|
||||
component: { template: '<div>Payment</div>' },
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async function mountPage(token = 'token-abc') {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const router = createTestRouter()
|
||||
await router.push({ name: 'guest-order', params: { token } })
|
||||
await router.isReady()
|
||||
|
||||
const wrapper = mount(GuestOrderPage, {
|
||||
global: { plugins: [router, pinia] },
|
||||
})
|
||||
|
||||
return { wrapper, router }
|
||||
}
|
||||
|
||||
describe('GuestOrderPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue(mockOrder)
|
||||
})
|
||||
|
||||
it('shows loading state initially', async () => {
|
||||
// Never resolve so we can see the loading state
|
||||
mocks.mockFetchGuestOrder.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
const { wrapper } = await mountPage()
|
||||
expect(wrapper.text()).toContain('Laddar')
|
||||
})
|
||||
|
||||
it('renders order details after loading', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Din beställning')
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('ABC123')
|
||||
expect(wrapper.text()).toContain('order-123')
|
||||
expect(wrapper.text()).toContain('Hej, din bil står fel!')
|
||||
})
|
||||
|
||||
it('displays human-readable status labels', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'pending_payment',
|
||||
})
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Väntar på betalning')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows "Behandlas" for paid status', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'paid',
|
||||
})
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Behandlas')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows "Skickat" for sent status', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'sent',
|
||||
})
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Skickat')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows "Levererat" for delivered status', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'delivered',
|
||||
})
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Levererat')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows "Misslyckades" for failed status', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'failed',
|
||||
})
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Misslyckades')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows "Avbrutet" for cancelled status', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'cancelled',
|
||||
})
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Avbrutet')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows payment link when status is pending_payment', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Gå till betalningssidan')
|
||||
})
|
||||
})
|
||||
|
||||
it('hides payment link when order is not pending_payment', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'sent',
|
||||
})
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).not.toContain('Gå till betalningssidan')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error when fetch fails', async () => {
|
||||
mocks.mockFetchGuestOrder.mockRejectedValue(new Error('Not found'))
|
||||
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Kunde inte hitta beställningen. Kontrollera länken.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not fetch order when token is missing in params', async () => {
|
||||
// The component guards against an empty token in onMounted.
|
||||
// Vue Router requires the :token param so we simulate by checking
|
||||
// that fetchGuestOrder is only called once per mount with a token.
|
||||
const { wrapper } = await mountPage('token-abc')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Din beställning')
|
||||
})
|
||||
expect(mocks.mockFetchGuestOrder).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
307
frontend/src/__tests__/GuestPaymentRedirect.spec.ts
Normal file
307
frontend/src/__tests__/GuestPaymentRedirect.spec.ts
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockFetchGuestOrder: vi.fn(),
|
||||
mockPayGuestOrder: vi.fn(),
|
||||
mockFetchSwishInfo: vi.fn(),
|
||||
mockBuildSwishPaymentUrl: vi.fn(),
|
||||
mockToDataURL: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/guestOrders', () => ({
|
||||
fetchGuestOrder: mocks.mockFetchGuestOrder,
|
||||
payGuestOrder: mocks.mockPayGuestOrder,
|
||||
}))
|
||||
|
||||
vi.mock('@/api/payment', () => ({
|
||||
fetchSwishInfo: mocks.mockFetchSwishInfo,
|
||||
buildSwishPaymentUrl: mocks.mockBuildSwishPaymentUrl,
|
||||
}))
|
||||
|
||||
vi.mock('qrcode', () => ({
|
||||
default: {
|
||||
toDataURL: mocks.mockToDataURL,
|
||||
},
|
||||
}))
|
||||
|
||||
import GuestPaymentRedirect from '@/pages/GuestPaymentRedirect.vue'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const mockToDataURL = vi.mocked(QRCode.toDataURL)
|
||||
|
||||
const mockOrder = {
|
||||
id: 'order-123',
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej',
|
||||
status: 'pending_payment',
|
||||
trackingId: null,
|
||||
amountPaid: null,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
guestToken: 'token-abc',
|
||||
}
|
||||
|
||||
function createTestRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
||||
{
|
||||
path: '/gast-betalning/:orderId',
|
||||
name: 'guest-payment',
|
||||
component: GuestPaymentRedirect,
|
||||
},
|
||||
{
|
||||
path: '/gast-order/:token',
|
||||
name: 'guest-order',
|
||||
component: { template: '<div>Order</div>' },
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async function mountPage(
|
||||
orderId = 'order-123',
|
||||
token = 'token-abc',
|
||||
plate = 'ABC123',
|
||||
) {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const router = createTestRouter()
|
||||
await router.push({
|
||||
name: 'guest-payment',
|
||||
params: { orderId },
|
||||
query: { token, plate },
|
||||
})
|
||||
await router.isReady()
|
||||
|
||||
const wrapper = mount(GuestPaymentRedirect, {
|
||||
global: { plugins: [router, pinia] },
|
||||
})
|
||||
|
||||
return { wrapper, router }
|
||||
}
|
||||
|
||||
function setupDefaultMocks() {
|
||||
mocks.mockFetchSwishInfo.mockResolvedValue({
|
||||
number: '0701234567',
|
||||
amount: 49,
|
||||
})
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue(mockOrder)
|
||||
mocks.mockBuildSwishPaymentUrl.mockReturnValue(
|
||||
'https://app.swish.nu/1/p/sw/?sw=46701234567&amt=49.00&msg=order-123',
|
||||
)
|
||||
mocks.mockToDataURL.mockResolvedValue('data:image/png;base64,mock-qr')
|
||||
}
|
||||
|
||||
describe('GuestPaymentRedirect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupDefaultMocks()
|
||||
})
|
||||
|
||||
it('renders heading and plate', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Betalning')
|
||||
})
|
||||
expect(wrapper.text()).toContain('ABC123')
|
||||
})
|
||||
|
||||
it('displays order id', async () => {
|
||||
const { wrapper } = await mountPage('order-456')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('order-456')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the amount to pay', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('49 kr')
|
||||
})
|
||||
})
|
||||
|
||||
it('renders QR code after loading swish info', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__qr-img').exists()).toBe(true)
|
||||
})
|
||||
expect(mockToDataURL).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('renders Swish payment link', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
const link = wrapper.find('.payment__swish-link')
|
||||
expect(link.exists()).toBe(true)
|
||||
expect(link.attributes('href')).toContain('app.swish.nu')
|
||||
})
|
||||
})
|
||||
|
||||
it('displays Swish number from API', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('0701234567')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the "Jag har betalat" button', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__submit').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Jag har betalat')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows confirmation dialog when clicking "Jag har betalat"', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__submit').exists()).toBe(true)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__submit').trigger('click')
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Jag bekräftar att jag har Swishat')
|
||||
expect(wrapper.text()).toContain('0701234567')
|
||||
})
|
||||
})
|
||||
|
||||
it('can cancel the confirmation dialog', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__submit').exists()).toBe(true)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__submit').trigger('click')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__confirm-cancel').exists()).toBe(true)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__confirm-cancel').trigger('click')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Jag har betalat')
|
||||
expect(wrapper.text()).not.toContain('Jag bekräftar')
|
||||
})
|
||||
})
|
||||
|
||||
it('calls payGuestOrder on confirmation', async () => {
|
||||
mocks.mockPayGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'processing',
|
||||
})
|
||||
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__submit').exists()).toBe(true)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__submit').trigger('click')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__confirm .btn--primary').exists()).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__confirm .btn--primary').trigger('click')
|
||||
|
||||
expect(mocks.mockPayGuestOrder).toHaveBeenCalledWith('token-abc')
|
||||
})
|
||||
|
||||
it('navigates to guest-order after successful payment', async () => {
|
||||
mocks.mockPayGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'processing',
|
||||
})
|
||||
|
||||
const { wrapper, router } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__submit').exists()).toBe(true)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__submit').trigger('click')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__confirm .btn--primary').exists()).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__confirm .btn--primary').trigger('click')
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(router.currentRoute.value.name).toBe('guest-order')
|
||||
expect(router.currentRoute.value.params.token).toBe('token-abc')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error when payment confirmation fails', async () => {
|
||||
mocks.mockPayGuestOrder.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__submit').exists()).toBe(true)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__submit').trigger('click')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.find('.payment__confirm .btn--primary').exists()).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
await wrapper.find('.payment__confirm .btn--primary').trigger('click')
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Kunde inte bekräfta betalningen. Försök igen.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error when swish info fetch fails', async () => {
|
||||
mocks.mockFetchSwishInfo.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Kunde inte ladda betalningsinformation. Försök igen senare.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error when token is missing', async () => {
|
||||
const { wrapper } = await mountPage('order-123', '', 'ABC123')
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Saknar order-token. Gå tillbaka och försök igen.',
|
||||
)
|
||||
})
|
||||
expect(mocks.mockFetchSwishInfo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('redirects to guest-order when order is already paid', async () => {
|
||||
mocks.mockFetchGuestOrder.mockResolvedValue({
|
||||
...mockOrder,
|
||||
status: 'processing',
|
||||
})
|
||||
|
||||
const { router } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(router.currentRoute.value.name).toBe('guest-order')
|
||||
expect(router.currentRoute.value.params.token).toBe('token-abc')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the magic order link', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('Din orderlänk')
|
||||
})
|
||||
expect(wrapper.find('.guest-payment__magic-url').text()).toContain(
|
||||
'token-abc',
|
||||
)
|
||||
})
|
||||
})
|
||||
104
frontend/src/__tests__/guestOrders.spec.ts
Normal file
104
frontend/src/__tests__/guestOrders.spec.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockRequest: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/client', () => ({
|
||||
request: mocks.mockRequest,
|
||||
}))
|
||||
|
||||
import {
|
||||
createGuestOrder,
|
||||
fetchGuestOrder,
|
||||
payGuestOrder,
|
||||
} from '@/api/guestOrders'
|
||||
|
||||
describe('guestOrders API', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mocks.mockRequest.mockReset()
|
||||
})
|
||||
|
||||
describe('createGuestOrder', () => {
|
||||
it('sends a POST to /guest-orders with plate, letterText, and email', async () => {
|
||||
const mockOrder = {
|
||||
id: 'order-1',
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej',
|
||||
status: 'pending_payment',
|
||||
trackingId: null,
|
||||
amountPaid: null,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
guestToken: 'token-abc',
|
||||
}
|
||||
mocks.mockRequest.mockResolvedValue(mockOrder)
|
||||
|
||||
const result = await createGuestOrder('ABC123', 'Hej', 'test@example.se')
|
||||
|
||||
expect(mocks.mockRequest).toHaveBeenCalledWith('/guest-orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej',
|
||||
email: 'test@example.se',
|
||||
}),
|
||||
})
|
||||
expect(result).toEqual(mockOrder)
|
||||
})
|
||||
|
||||
it('propagates errors from the API client', async () => {
|
||||
mocks.mockRequest.mockRejectedValue(new Error('Server error'))
|
||||
|
||||
await expect(
|
||||
createGuestOrder('ABC123', 'Hej', 'test@example.se'),
|
||||
).rejects.toThrow('Server error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchGuestOrder', () => {
|
||||
it('sends a GET to /guest-orders/:token', async () => {
|
||||
const mockOrder = {
|
||||
id: 'order-1',
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej',
|
||||
status: 'pending_payment',
|
||||
trackingId: null,
|
||||
amountPaid: null,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
guestToken: 'token-abc',
|
||||
}
|
||||
mocks.mockRequest.mockResolvedValue(mockOrder)
|
||||
|
||||
const result = await fetchGuestOrder('token-abc')
|
||||
|
||||
expect(mocks.mockRequest).toHaveBeenCalledWith('/guest-orders/token-abc')
|
||||
expect(result).toEqual(mockOrder)
|
||||
})
|
||||
})
|
||||
|
||||
describe('payGuestOrder', () => {
|
||||
it('sends a POST to /guest-orders/:token/pay', async () => {
|
||||
const mockOrder = {
|
||||
id: 'order-1',
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej',
|
||||
status: 'processing',
|
||||
trackingId: null,
|
||||
amountPaid: 49,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
guestToken: 'token-abc',
|
||||
}
|
||||
mocks.mockRequest.mockResolvedValue(mockOrder)
|
||||
|
||||
const result = await payGuestOrder('token-abc')
|
||||
|
||||
expect(mocks.mockRequest).toHaveBeenCalledWith(
|
||||
'/guest-orders/token-abc/pay',
|
||||
{ method: 'POST' },
|
||||
)
|
||||
expect(result).toEqual(mockOrder)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue