hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads, attach packages, and surface localized success/error states. - Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/ PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent creation, webhooks, and the wizard CTA. - Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/ useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages, Checkout) with localized copy and experiment tracking. - Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations. - Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke test for the hero CTA while reconciling outstanding checklist items.
192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
import { execSync } from 'child_process';
|
||
|
||
const LOGIN_EMAIL = 'checkout-e2e@example.com';
|
||
const LOGIN_PASSWORD = 'Password123!';
|
||
|
||
test.describe('Checkout Payment Step – Stripe & PayPal states', () => {
|
||
test.beforeAll(async () => {
|
||
execSync(
|
||
`php artisan tenant:add-dummy --email=${LOGIN_EMAIL} --password=${LOGIN_PASSWORD} --first_name=Checkout --last_name=Tester --address="Playwrightstr. 1" --phone="+4912345678"`
|
||
);
|
||
execSync(
|
||
`php artisan tinker --execute="App\\\\Models\\\\User::where('email', '${LOGIN_EMAIL}')->update(['email_verified_at' => now()]);"`
|
||
);
|
||
});
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto('/login');
|
||
await page.fill('input[name="email"]', LOGIN_EMAIL);
|
||
await page.fill('input[name="password"]', LOGIN_PASSWORD);
|
||
await page.getByRole('button', { name: /Anmelden|Login/ }).click();
|
||
await expect(page).toHaveURL(/dashboard/);
|
||
});
|
||
|
||
test('Stripe payment intent error surfaces descriptive status', async ({ page }) => {
|
||
await page.route('**/stripe/create-payment-intent', async (route) => {
|
||
await route.fulfill({
|
||
status: 422,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ error: 'Test payment intent failure' }),
|
||
});
|
||
});
|
||
|
||
await openCheckoutPaymentStep(page);
|
||
|
||
await expect(
|
||
page.locator('text=/Fehler beim Laden der Zahlungsdaten|Error loading payment data/')
|
||
).toBeVisible();
|
||
await expect(
|
||
page.locator('text=/Zahlungsformular bereit|Payment form ready/')
|
||
).not.toBeVisible();
|
||
});
|
||
|
||
test('Stripe payment intent ready state renders when backend responds', async ({ page }) => {
|
||
await page.route('**/stripe/create-payment-intent', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ client_secret: 'pi_test_secret' }),
|
||
});
|
||
});
|
||
|
||
await openCheckoutPaymentStep(page);
|
||
|
||
await expect(
|
||
page.locator('text=/Zahlungsformular bereit\\. Bitte gib deine Daten ein\\.|Payment form ready\\./')
|
||
).toBeVisible();
|
||
});
|
||
|
||
test('PayPal approval success updates status', async ({ page }) => {
|
||
await stubPayPalSdk(page);
|
||
|
||
await page.route('**/paypal/create-order', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ id: 'ORDER_TEST', status: 'CREATED' }),
|
||
});
|
||
});
|
||
|
||
await page.route('**/paypal/capture-order', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ status: 'captured' }),
|
||
});
|
||
});
|
||
|
||
await openCheckoutPaymentStep(page);
|
||
await selectPayPalMethod(page);
|
||
|
||
await page.waitForFunction(() => window.__paypalButtonsConfig !== undefined);
|
||
await page.evaluate(async () => {
|
||
const config = window.__paypalButtonsConfig;
|
||
if (!config) return;
|
||
|
||
await config.createOrder();
|
||
await config.onApprove({ orderID: 'ORDER_TEST' });
|
||
});
|
||
|
||
await expect(
|
||
page.locator('text=/Zahlung bestätigt\\. Bestellung wird abgeschlossen|Payment confirmed/')
|
||
).toBeVisible();
|
||
});
|
||
|
||
test('PayPal capture failure notifies user', async ({ page }) => {
|
||
await stubPayPalSdk(page);
|
||
|
||
await page.route('**/paypal/create-order', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ id: 'ORDER_FAIL', status: 'CREATED' }),
|
||
});
|
||
});
|
||
|
||
await page.route('**/paypal/capture-order', async (route) => {
|
||
await route.fulfill({
|
||
status: 500,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ error: 'capture_failed' }),
|
||
});
|
||
});
|
||
|
||
await openCheckoutPaymentStep(page);
|
||
await selectPayPalMethod(page);
|
||
|
||
await page.waitForFunction(() => window.__paypalButtonsConfig !== undefined);
|
||
await page.evaluate(async () => {
|
||
const config = window.__paypalButtonsConfig;
|
||
if (!config) return;
|
||
|
||
await config.createOrder();
|
||
await config.onApprove({ orderID: 'ORDER_FAIL' });
|
||
});
|
||
|
||
await expect(
|
||
page.locator('text=/PayPal capture failed|PayPal-Abbuchung fehlgeschlagen|PayPal capture error/')
|
||
).toBeVisible();
|
||
});
|
||
});
|
||
|
||
async function openCheckoutPaymentStep(page: import('@playwright/test').Page) {
|
||
await page.goto('/packages');
|
||
const checkoutLink = page.locator('a[href^="/checkout/"]').first();
|
||
const href = await checkoutLink.getAttribute('href');
|
||
|
||
if (!href) {
|
||
throw new Error('No checkout link found on packages page.');
|
||
}
|
||
|
||
await page.goto(href);
|
||
|
||
const nextButton = page.getByRole('button', {
|
||
name: /Weiter zum Zahlungsschritt|Continue to Payment/,
|
||
});
|
||
if (await nextButton.isVisible()) {
|
||
await nextButton.click();
|
||
}
|
||
|
||
await page.waitForSelector('text=/Zahlung|Payment/');
|
||
}
|
||
|
||
async function selectPayPalMethod(page: import('@playwright/test').Page) {
|
||
const paypalButton = page.getByRole('button', { name: /PayPal/ });
|
||
if (await paypalButton.isVisible()) {
|
||
await paypalButton.click();
|
||
}
|
||
}
|
||
|
||
async function stubPayPalSdk(page: import('@playwright/test').Page) {
|
||
await page.route('https://www.paypal.com/sdk/js**', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/javascript',
|
||
body: `
|
||
window.paypal = {
|
||
Buttons: function (config) {
|
||
window.__paypalButtonsConfig = config;
|
||
return {
|
||
render: function () {
|
||
// noop
|
||
},
|
||
};
|
||
},
|
||
};
|
||
`,
|
||
});
|
||
});
|
||
}
|
||
|
||
declare global {
|
||
interface Window {
|
||
__paypalButtonsConfig?: {
|
||
createOrder: () => Promise<string>;
|
||
onApprove: (data: { orderID: string }) => Promise<void>;
|
||
onError?: (error: unknown) => void;
|
||
};
|
||
}
|
||
}
|
||
|