test(guest): add frontend unit tests for guest checkout flow
Add 41 tests covering all four guest checkout modules that had 0% function coverage in CI, raising overall frontend coverage from 76.69% to 84.64% statements, well above the 70% threshold. New test files: - guestOrders.spec.ts: verify createGuestOrder posts correct body, fetchGuestOrder GETs /guest-orders/:token, payGuestOrder POSTs /guest-orders/:token/pay; error propagation - GuestCheckoutPage.spec.ts: form validation (plate regex, email regex, button disabled state), character counter, successful submission navigates to guest-payment with token+plate query, API failure shows Swedish error message - GuestOrderPage.spec.ts: loading state, order detail rendering, all six status labels (pending_payment, paid, sent, delivered, failed, cancelled), payment link visibility, fetch error handling - GuestPaymentRedirect.spec.ts: Swish number/amount display, QR code rendering, Swish payment link, confirmation dialog flow, payGuestOrder call on confirm, navigation on success, error for missing token and failed fetch, redirect when already paid, magic order link Also includes a rebase onto origin/master (PR #16 Swish QR fixes, payment unit tests, e2e retry improvements) which resolves the 'not up to date with master' from the review comment.
This commit is contained in:
parent
d16f048630
commit
2ef093ba2b
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