249 lines
8.2 KiB
TypeScript
249 lines
8.2 KiB
TypeScript
import type { Page } from '@playwright/test';
|
|
import fs from 'node:fs/promises';
|
|
|
|
import { dismissConsentBanner, expectFixture as expect, test } from '../helpers/test-fixtures';
|
|
|
|
const shouldRun =
|
|
process.env.E2E_PAYPAL_SANDBOX === '1' ||
|
|
process.env.E2E_LEMONSQUEEZY_SANDBOX === '1' ||
|
|
process.env.E2E_PADDLE_SANDBOX === '1';
|
|
const baseUrl = process.env.E2E_BASE_URL ?? 'https://test-y0k0.fotospiel.app';
|
|
const locale = process.env.E2E_LOCALE ?? 'de';
|
|
const checkoutSlug = locale === 'en' ? 'checkout' : 'bestellen';
|
|
const tenantEmail = buildTenantEmail();
|
|
const tenantPassword = process.env.E2E_TENANT_PASSWORD ?? null;
|
|
|
|
test.use({
|
|
channel: process.env.E2E_BROWSER_CHANNEL ?? 'chrome',
|
|
userAgent:
|
|
process.env.E2E_USER_AGENT ??
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
launchOptions: {
|
|
args: ['--disable-blink-features=AutomationControlled'],
|
|
},
|
|
});
|
|
|
|
test.describe('PayPal sandbox full flow (staging)', () => {
|
|
test.skip(!shouldRun, 'Set E2E_PAYPAL_SANDBOX=1 to run sandbox checkout on staging.');
|
|
test.skip(!tenantEmail || !tenantPassword, 'Set E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD for sandbox flow.');
|
|
|
|
test('register, simulate PayPal completion, and login to event admin', async ({ page }, testInfo) => {
|
|
const paypalNetworkLog: string[] = [];
|
|
|
|
page.on('response', async (response) => {
|
|
const url = response.url();
|
|
if (!/paypal/i.test(url)) {
|
|
return;
|
|
}
|
|
|
|
const status = response.status();
|
|
let bodySnippet = '';
|
|
if (status >= 400 || /checkout|error/i.test(url)) {
|
|
try {
|
|
const text = await response.text();
|
|
bodySnippet = text.trim().slice(0, 2000);
|
|
} catch {
|
|
bodySnippet = '';
|
|
}
|
|
}
|
|
|
|
const entry = [`[${status}] ${url}`, bodySnippet].filter(Boolean).join('
|
|
');
|
|
paypalNetworkLog.push(entry);
|
|
if (paypalNetworkLog.length > 40) {
|
|
paypalNetworkLog.shift();
|
|
}
|
|
});
|
|
|
|
await page.addInitScript(() => {
|
|
Object.defineProperty(navigator, 'webdriver', {
|
|
get: () => undefined,
|
|
});
|
|
});
|
|
|
|
await page.route('https://www.paypal.com/sdk/js**', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/javascript',
|
|
body: `
|
|
window.paypal = {
|
|
Buttons: function(options) {
|
|
window.__paypalOptions = options;
|
|
return {
|
|
render: function() { return Promise.resolve(); },
|
|
};
|
|
},
|
|
};
|
|
`,
|
|
});
|
|
});
|
|
|
|
await page.route('**/paypal/create-order', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
order_id: 'order_test_123',
|
|
checkout_session_id: 'session_test_123',
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route('**/paypal/capture-order', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
status: 'completed',
|
|
}),
|
|
});
|
|
});
|
|
|
|
try {
|
|
await page.goto(`${baseUrl}/${locale}/${checkoutSlug}/2`);
|
|
|
|
await dismissConsentBanner(page);
|
|
|
|
await proceedToAccountStep(page);
|
|
|
|
await completeRegistrationOrLogin(page, {
|
|
email: tenantEmail!,
|
|
password: tenantPassword!,
|
|
});
|
|
|
|
const termsCheckbox = page.locator('#checkout-terms-hero');
|
|
await expect(termsCheckbox).toBeVisible();
|
|
await termsCheckbox.click();
|
|
|
|
await expect.poll(async () => {
|
|
return page.evaluate(() => Boolean(window.__paypalOptions));
|
|
}).toBe(true);
|
|
|
|
await page.evaluate(async () => {
|
|
await window.__paypalOptions?.createOrder?.();
|
|
await window.__paypalOptions?.onApprove?.({ orderID: 'order_test_123' });
|
|
});
|
|
|
|
await expect(page.getByText(/Marketing-Dashboard/)).toBeVisible();
|
|
|
|
await page.goto(`${baseUrl}/event-admin/login`);
|
|
await dismissConsentBanner(page);
|
|
|
|
await page.locator('input[name="identifier"]').fill(tenantEmail!);
|
|
await page.locator('input[name="password"]').fill(tenantPassword!);
|
|
await page.getByRole('button', { name: /Anmelden|Login/i }).click();
|
|
|
|
await expect(page).toHaveURL(/\/event-admin\/mobile\/(dashboard|welcome)/i, { timeout: 30_000 });
|
|
await expect(page.getByText(/Dashboard|Willkommen/i)).toBeVisible();
|
|
} finally {
|
|
if (paypalNetworkLog.length > 0) {
|
|
const logPath = testInfo.outputPath('paypal-network-log.txt');
|
|
await fs.writeFile(logPath, paypalNetworkLog.join('
|
|
|
|
'), 'utf8');
|
|
await testInfo.attach('paypal-network-log', {
|
|
path: logPath,
|
|
contentType: 'text/plain',
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
async function completeRegistrationOrLogin(page: Page, credentials: { email: string; password: string }): Promise<void> {
|
|
const identifierInput = page.locator('input[name="identifier"]');
|
|
if (await identifierInput.isVisible()) {
|
|
await identifierInput.fill(credentials.email);
|
|
await page.locator('input[name="password"]').fill(credentials.password);
|
|
await page.getByRole('button', { name: /^Anmelden$|Login/i }).last().click();
|
|
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
|
return;
|
|
}
|
|
|
|
await page.fill('input[name="first_name"]', 'Play');
|
|
await page.fill('input[name="last_name"]', 'Wright');
|
|
await page.fill('input[name="email"]', credentials.email);
|
|
const addressInput = page.locator('input[name="address"]');
|
|
if (await addressInput.isVisible()) {
|
|
await addressInput.fill('Teststrasse 1, 12345 Berlin');
|
|
}
|
|
|
|
const phoneInput = page.locator('input[name="phone"]');
|
|
if (await phoneInput.isVisible()) {
|
|
await phoneInput.fill('+49123456789');
|
|
}
|
|
|
|
const usernameInput = page.locator('input[name="username"]');
|
|
if (await usernameInput.isVisible()) {
|
|
await usernameInput.fill(credentials.email);
|
|
}
|
|
await page.fill('input[name="password"]', credentials.password);
|
|
await page.fill('input[name="password_confirmation"]', credentials.password);
|
|
|
|
await page.check('input[name="privacy_consent"]');
|
|
await page.getByRole('button', { name: /Registrieren|Register/i }).last().click();
|
|
|
|
try {
|
|
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
|
return;
|
|
} catch {
|
|
const loginButton = page.getByRole('button', { name: /^Anmelden$|Login/i }).first();
|
|
if (await loginButton.isVisible()) {
|
|
await loginButton.click();
|
|
}
|
|
}
|
|
|
|
await expect(page.locator('input[name="identifier"]')).toBeVisible({ timeout: 10_000 });
|
|
await page.locator('input[name="identifier"]').fill(credentials.email);
|
|
await page.locator('input[name="password"]').fill(credentials.password);
|
|
await page.getByRole('button', { name: /^Anmelden$|Login/i }).last().click();
|
|
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
|
}
|
|
|
|
async function proceedToAccountStep(page: Page, timeoutMs = 30_000): Promise<void> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
while (Date.now() < deadline) {
|
|
const accountForm = page.locator('input[name="first_name"], input[name="identifier"]');
|
|
if (await accountForm.isVisible()) {
|
|
return;
|
|
}
|
|
|
|
await dismissConsentBanner(page);
|
|
|
|
const continueButton = page.getByRole('button', { name: /Weiter|Continue/i }).first();
|
|
if (await continueButton.isVisible()) {
|
|
if (await continueButton.isEnabled()) {
|
|
await continueButton.click();
|
|
}
|
|
}
|
|
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
throw new Error('Account step did not load in time.');
|
|
}
|
|
|
|
function buildTenantEmail(): string | null {
|
|
const rawEmail = process.env.E2E_TENANT_EMAIL ?? process.env.E2E_PAYPAL_EMAIL ?? process.env.E2E_LEMONSQUEEZY_EMAIL ?? process.env.E2E_PADDLE_EMAIL ?? null;
|
|
if (!rawEmail) {
|
|
return null;
|
|
}
|
|
|
|
if (!rawEmail.includes('{timestamp}')) {
|
|
return rawEmail;
|
|
}
|
|
|
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
return rawEmail.replaceAll('{timestamp}', timestamp);
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__paypalOptions?: {
|
|
createOrder?: () => Promise<string>;
|
|
onApprove?: (data: { orderID: string }) => Promise<void>;
|
|
};
|
|
}
|
|
}
|