test: add payment flow tests and fix strict-mode e2e violations

Vitest:
  - PaymentRedirect.spec.ts (8 tests): renders heading and 49 kr,
    shows plate from query, Betalt button exists, calls payOrder on
    click, navigates to /orders on success, shows error on failure,
    disables button while paying, shows mock note
  - ComposePage.spec.ts: update navigation test to expect /betalning
    route with orderId param instead of /orders; add payment route
    to test router; add PaymentRedirect import

Playwright E2E:
  - payment-redirect.spec.ts (4 tests): compose→payment navigation,
    Betalt→orders flow, auth guard redirects to login, mock note
    visible
  - compose.spec.ts: rename test and update assertion from /orders
    to /betalning/ URL pattern; use getByRole('heading',
    { name: 'Betalning' }) to avoid strict mode violation with
    mock-note paragraph containing the word 'Betalning'
This commit is contained in:
Joakim Mörling 2026-05-15 20:31:16 +02:00
parent c3c1513ac1
commit 8cd7991603
6 changed files with 208 additions and 13 deletions

View file

@ -43,8 +43,8 @@ test.describe('Admin dashboard', () => {
await page.goto('/admin')
await expect(page.locator('.admin-dashboard__plate').first()).toBeVisible()
await expect(page.getByText('DEF456')).toBeVisible()
await expect(page.getByText('GHI789')).toBeVisible()
await expect(page.getByText('DEF456').first()).toBeVisible()
await expect(page.getByText('GHI789').first()).toBeVisible()
})
test('click row expands letter content', async ({ page }) => {
@ -109,7 +109,7 @@ test.describe('Admin dashboard', () => {
test('hides PostNord link when trackingId is null', async ({ page }) => {
await page.goto('/admin')
const defRow = page.locator('.admin-dashboard__row', { hasText: 'DEF456' })
const defRow = page.locator('.admin-dashboard__row', { hasText: 'DEF456' }).first()
await defRow.click()
const trackingLink = page.locator('.admin-dashboard__tracking-link')

View file

@ -48,7 +48,7 @@ test.describe('Compose flow', () => {
await expect(button).toBeDisabled()
})
test('can create order and navigate to orders page', async ({ page }) => {
test('can create order and navigate to payment page', async ({ page }) => {
await page.goto('/logga-in')
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
await page.getByLabel('Lösenord').fill('test1234')
@ -62,10 +62,9 @@ test.describe('Compose flow', () => {
await expect(button).toBeEnabled()
await button.click()
await expect(page).toHaveURL('/orders')
await expect(
page.getByRole('heading', { name: 'Mina beställningar' }),
).toBeVisible()
await expect(page).toHaveURL(/\/betalning\//)
await expect(page.getByRole('heading', { name: 'Betalning' })).toBeVisible()
await expect(page.getByText('49 kr')).toBeVisible()
})
test('preview shows letter content and GDPR footer', async ({ page }) => {

View file

@ -36,8 +36,8 @@ test.describe('Order history', () => {
await expect(page.getByRole('heading', { name: 'Mina beställningar' })).toBeVisible()
await expect(page.getByText('ABC123').first()).toBeVisible()
await expect(page.getByText('DEF456')).toBeVisible()
await expect(page.getByText('GHI789')).toBeVisible()
await expect(page.getByText('DEF456').first()).toBeVisible()
await expect(page.getByText('GHI789').first()).toBeVisible()
})
test('shows correct status badges', async ({ page }) => {
@ -50,7 +50,7 @@ test.describe('Order history', () => {
await page.goto('/orders')
await expect(page.getByText('Skickat')).toBeVisible()
await expect(page.getByText('Väntar på betalning')).toBeVisible()
await expect(page.getByText('Väntar på betalning').first()).toBeVisible()
await expect(page.getByText('Levererat').first()).toBeVisible()
})

View file

@ -0,0 +1,52 @@
import { test, expect } from '@playwright/test'
test.describe('Payment redirect', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/logga-in')
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
await page.getByLabel('Lösenord').fill('test1234')
await page.getByRole('button', { name: 'Logga in' }).click()
await page.waitForURL('/')
})
test('can navigate to payment page from compose', async ({ page }) => {
await page.goto('/compose?plate=ABC123')
await page.getByLabel('Ditt meddelande').fill('Hej fin bil!')
await page.getByRole('button', { name: 'Skicka brev (49 kr)' }).click()
await expect(page).toHaveURL(/\/betalning\//)
await expect(page.getByRole('heading', { name: 'Betalning' })).toBeVisible()
await expect(page.getByText('49 kr')).toBeVisible()
await expect(page.getByText('ABC123')).toBeVisible()
})
test('Betalt button marks order as paid and redirects to orders', async ({
page,
}) => {
await page.goto('/compose?plate=DEF456')
await page.getByLabel('Ditt meddelande').fill('Vill köpa din bil.')
await page.getByRole('button', { name: 'Skicka brev (49 kr)' }).click()
await page.waitForURL(/\/betalning\//)
await page.getByRole('button', { name: 'Betalt' }).click()
await expect(page).toHaveURL('/orders')
await expect(page.getByText('DEF456').first()).toBeVisible()
})
test('payment page requires authentication', async ({ page }) => {
await page.evaluate(() => localStorage.clear())
await page.goto('/betalning/some-id')
await expect(page).toHaveURL(/\/logga-in/)
})
test('shows mock payment note', async ({ page }) => {
await page.goto('/compose?plate=GHI789')
await page.getByLabel('Ditt meddelande').fill('Hej!')
await page.getByRole('button', { name: 'Skicka brev (49 kr)' }).click()
await page.waitForURL(/\/betalning\//)
await expect(page.getByText(/mock-betalning/i)).toBeVisible()
})
})

View file

@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { createRouter, createMemoryHistory } from 'vue-router'
import ComposePage from '@/pages/ComposePage.vue'
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
vi.mock('@/api/orders', () => ({
createOrder: vi.fn(),
@ -31,6 +32,11 @@ function createTestRouter() {
name: 'orders',
component: { template: '<div>Orders</div>' },
},
{
path: '/betalning/:orderId',
name: 'payment',
component: PaymentRedirect,
},
],
})
}
@ -122,12 +128,13 @@ describe('ComposePage', () => {
})
})
it('navigates to /orders on success', async () => {
it('navigates to payment on success', async () => {
mockCreateOrder.mockResolvedValue({
id: 'order-1',
plate: 'ABC123',
status: 'pending_payment',
trackingId: null,
amountPaid: null,
createdAt: '2025-01-01T00:00:00Z',
})
@ -138,7 +145,8 @@ describe('ComposePage', () => {
await button.trigger('submit')
await vi.waitFor(() => {
expect(router.currentRoute.value.name).toBe('orders')
expect(router.currentRoute.value.name).toBe('payment')
expect(router.currentRoute.value.params.orderId).toBe('order-1')
})
})

View file

@ -0,0 +1,136 @@
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'
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
import OrdersPage from '@/pages/OrdersPage.vue'
vi.mock('@/api/payment', () => ({
payOrder: vi.fn(),
}))
import { payOrder } from '@/api/payment'
const mockPayOrder = vi.mocked(payOrder)
function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
{
path: '/betalning/:orderId',
name: 'payment',
component: PaymentRedirect,
},
{
path: '/orders',
name: 'orders',
component: OrdersPage,
},
],
})
}
async function mountPage(orderId = 'order-1', plate = 'ABC123') {
const pinia = createPinia()
setActivePinia(pinia)
const router = createTestRouter()
await router.push({
name: 'payment',
params: { orderId },
query: { plate },
})
await router.isReady()
const wrapper = mount(PaymentRedirect, {
global: { plugins: [router, pinia] },
})
return { wrapper, router }
}
describe('PaymentRedirect', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renders heading and amount', async () => {
const { wrapper } = await mountPage()
expect(wrapper.text()).toContain('Betalning')
expect(wrapper.text()).toContain('49 kr')
})
it('shows plate from query', async () => {
const { wrapper } = await mountPage('order-1', 'ABC123')
expect(wrapper.text()).toContain('ABC123')
})
it('shows Betalt button', async () => {
const { wrapper } = await mountPage()
const button = wrapper.find('.payment__button')
expect(button.exists()).toBe(true)
expect(button.text()).toBe('Betalt')
})
it('shows mock payment note', async () => {
const { wrapper } = await mountPage()
expect(wrapper.text()).toContain('mock-betalning')
})
it('calls payOrder on button click', async () => {
mockPayOrder.mockResolvedValue({
id: 'order-1',
plate: 'ABC123',
status: 'paid',
trackingId: null,
amountPaid: 49.0,
createdAt: '2025-01-01T00:00:00Z',
})
const { wrapper } = await mountPage()
await wrapper.find('.payment__button').trigger('click')
expect(mockPayOrder).toHaveBeenCalledWith('order-1')
})
it('navigates to orders on success', async () => {
mockPayOrder.mockResolvedValue({
id: 'order-1',
plate: 'ABC123',
status: 'paid',
trackingId: null,
amountPaid: 49.0,
createdAt: '2025-01-01T00:00:00Z',
})
const { wrapper, router } = await mountPage()
await wrapper.find('.payment__button').trigger('click')
await vi.waitFor(() => {
expect(router.currentRoute.value.name).toBe('orders')
})
})
it('shows error on payment failure', async () => {
mockPayOrder.mockRejectedValue(new Error('Network error'))
const { wrapper } = await mountPage()
await wrapper.find('.payment__button').trigger('click')
await vi.waitFor(() => {
expect(wrapper.text()).toContain('Kunde inte genomföra betalningen')
})
})
it('disables button while paying', async () => {
mockPayOrder.mockImplementation(() => new Promise(() => {}))
const { wrapper } = await mountPage()
const button = wrapper.find('.payment__button')
await button.trigger('click')
expect(button.attributes('disabled')).toBeDefined()
expect(button.text()).toBe('Bearbetar...')
})
})