From 8cd7991603f604267c3fc1bf514cf5ebf62a4e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20M=C3=B6rling?= Date: Fri, 15 May 2026 20:31:16 +0200 Subject: [PATCH] test: add payment flow tests and fix strict-mode e2e violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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' --- frontend/e2e/admin-dashboard.spec.ts | 6 +- frontend/e2e/compose.spec.ts | 9 +- frontend/e2e/order-history.spec.ts | 6 +- frontend/e2e/payment-redirect.spec.ts | 52 +++++++ frontend/src/__tests__/ComposePage.spec.ts | 12 +- .../src/__tests__/PaymentRedirect.spec.ts | 136 ++++++++++++++++++ 6 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 frontend/e2e/payment-redirect.spec.ts create mode 100644 frontend/src/__tests__/PaymentRedirect.spec.ts diff --git a/frontend/e2e/admin-dashboard.spec.ts b/frontend/e2e/admin-dashboard.spec.ts index ccb7dee..aaadb25 100644 --- a/frontend/e2e/admin-dashboard.spec.ts +++ b/frontend/e2e/admin-dashboard.spec.ts @@ -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') diff --git a/frontend/e2e/compose.spec.ts b/frontend/e2e/compose.spec.ts index 38c2ba3..554558f 100644 --- a/frontend/e2e/compose.spec.ts +++ b/frontend/e2e/compose.spec.ts @@ -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 }) => { diff --git a/frontend/e2e/order-history.spec.ts b/frontend/e2e/order-history.spec.ts index 4ba3daa..82c3033 100644 --- a/frontend/e2e/order-history.spec.ts +++ b/frontend/e2e/order-history.spec.ts @@ -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() }) diff --git a/frontend/e2e/payment-redirect.spec.ts b/frontend/e2e/payment-redirect.spec.ts new file mode 100644 index 0000000..6b67437 --- /dev/null +++ b/frontend/e2e/payment-redirect.spec.ts @@ -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() + }) +}) diff --git a/frontend/src/__tests__/ComposePage.spec.ts b/frontend/src/__tests__/ComposePage.spec.ts index 3c1fc76..61d478c 100644 --- a/frontend/src/__tests__/ComposePage.spec.ts +++ b/frontend/src/__tests__/ComposePage.spec.ts @@ -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: '
Orders
' }, }, + { + 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') }) }) diff --git a/frontend/src/__tests__/PaymentRedirect.spec.ts b/frontend/src/__tests__/PaymentRedirect.spec.ts new file mode 100644 index 0000000..c1ba967 --- /dev/null +++ b/frontend/src/__tests__/PaymentRedirect.spec.ts @@ -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: '
Home
' } }, + { + 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...') + }) +})