Update Playwright staging flows and Paddle sandbox checkout
This commit is contained in:
@@ -1,59 +1,37 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expectFixture as expect } from '../helpers/test-fixtures';
|
||||
|
||||
test.describe('Package Flow in Admin PWA', () => {
|
||||
test('Create event with package and verify limits', async ({ page }) => {
|
||||
// Assume logged in as tenant admin, navigate to events page
|
||||
await page.goto('/event-admin/events');
|
||||
test('Create event in admin PWA and verify it appears in the list', async ({ tenantAdminCredentials, signInTenantAdmin, page }) => {
|
||||
test.skip(
|
||||
!tenantAdminCredentials,
|
||||
'Provide E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD to run admin package flow tests.'
|
||||
);
|
||||
|
||||
// Click create event button
|
||||
await page.click('[data-testid="create-event"]');
|
||||
await expect(page).toHaveURL(/\/event-admin\/events\/create/);
|
||||
await signInTenantAdmin();
|
||||
|
||||
// Fill form
|
||||
await page.fill('[name="name"]', 'Test Package Event');
|
||||
await page.fill('[name="slug"]', 'test-package-event');
|
||||
await page.fill('[name="date"]', '2025-10-01');
|
||||
await page.goto('/event-admin/mobile/events/new');
|
||||
|
||||
// Select package from dropdown
|
||||
await page.selectOption('[name="package_id"]', '1'); // Assume ID 1 is Starter package
|
||||
await expect(page.locator('[name="package_id"]')).toHaveValue('1');
|
||||
const eventName = `Package Flow ${Date.now()}`;
|
||||
const date = new Date(Date.now() + 14 * 86400000).toISOString().slice(0, 10);
|
||||
|
||||
// Submit
|
||||
await page.click('[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/event-admin\/events/);
|
||||
await page.getByPlaceholder(/Sommerfest|Summer Party/i).fill(eventName);
|
||||
await page.locator('input[type="datetime-local"]').fill(`${date}T12:00`);
|
||||
|
||||
// Verify event created and package assigned
|
||||
await expect(page.locator('text=Test Package Event')).toBeVisible();
|
||||
await expect(page.locator('text=Starter')).toBeVisible(); // Package name in table
|
||||
await page.locator('select').first().selectOption({ index: 1 });
|
||||
|
||||
// Check dashboard limits
|
||||
await page.goto('/event-admin/events');
|
||||
await expect(page.locator('text=Remaining Photos')).toContainText('300'); // Starter limit
|
||||
await page.getByRole('button', { name: /Event erstellen|Create event/i }).click();
|
||||
await expect(page).toHaveURL(/\/event-admin\/mobile\/events\/[a-z0-9-]+$/, { timeout: 20_000 });
|
||||
|
||||
// Try to create another event to test reseller limit if applicable
|
||||
// (Skip for endcustomer; assume tenant has reseller package with limit 1)
|
||||
await page.goto('/event-admin/events');
|
||||
await page.click('[data-testid="create-event"]');
|
||||
await page.fill('[name="name"]', 'Second Event');
|
||||
await page.fill('[name="slug"]', 'second-event');
|
||||
await page.fill('[name="date"]', '2025-10-02');
|
||||
await page.selectOption('[name="package_id"]', '1');
|
||||
await page.click('[type="submit"]');
|
||||
|
||||
// If limit reached, expect error
|
||||
await expect(page.locator('text=No available package')).toBeVisible();
|
||||
await page.goto('/event-admin/mobile/events');
|
||||
await expect(page.getByText(eventName, { exact: false })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Upload blocked when package limit reached in Guest PWA', async ({ page }) => {
|
||||
// Assume event with package limit 0 created
|
||||
await page.goto('/e/test-limited-event'); // Slug of event with max_photos = 0
|
||||
const eventToken = process.env.E2E_GUEST_LIMIT_EVENT_TOKEN;
|
||||
test.skip(!eventToken, 'Set E2E_GUEST_LIMIT_EVENT_TOKEN to a guest join token with exhausted limits.');
|
||||
|
||||
// Navigate to upload
|
||||
await page.click('text=Upload');
|
||||
await expect(page).toHaveURL(/\/upload/);
|
||||
await page.goto(`/e/${eventToken}/upload`);
|
||||
|
||||
// Expect upload disabled and error message
|
||||
await expect(page.locator('button:disabled')).toBeVisible(); // Upload button disabled
|
||||
await expect(page.locator('text=Upload-Limit erreicht')).toBeVisible();
|
||||
await expect(page.getByText(/Upload-Limit erreicht/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { test as base } from '../helpers/test-fixtures';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { dismissConsentBanner, expectFixture as expect, test } from '../helpers/test-fixtures';
|
||||
|
||||
const shouldRun = 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 sandboxEmail = process.env.E2E_PADDLE_EMAIL ?? 'playwright-buyer@example.com';
|
||||
const tenantEmail = process.env.E2E_TENANT_EMAIL ?? process.env.E2E_PADDLE_EMAIL ?? null;
|
||||
const tenantPassword = process.env.E2E_TENANT_PASSWORD ?? null;
|
||||
const sandboxCard = {
|
||||
number: process.env.E2E_PADDLE_CARD_NUMBER ?? '4242 4242 4242 4242',
|
||||
expiry: process.env.E2E_PADDLE_CARD_EXPIRY ?? '12/34',
|
||||
cvc: process.env.E2E_PADDLE_CARD_CVC ?? '123',
|
||||
name: process.env.E2E_PADDLE_CARD_NAME ?? 'Playwright Tester',
|
||||
postal: process.env.E2E_PADDLE_CARD_POSTAL ?? '10115',
|
||||
};
|
||||
|
||||
test.describe('Paddle sandbox full flow (staging)', () => {
|
||||
test.skip(!shouldRun, 'Set E2E_PADDLE_SANDBOX=1 to run live sandbox checkout on staging.');
|
||||
test.skip(!tenantEmail || !tenantPassword, 'Set E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD for sandbox flow.');
|
||||
|
||||
test('create checkout, simulate webhook completion, and verify session completion', async ({ page, request }) => {
|
||||
test('register, pay via Paddle sandbox, and login to event admin', async ({ page, request }) => {
|
||||
// Jump directly into wizard for Standard package (2)
|
||||
await page.goto(`${baseUrl}/${locale}/${checkoutSlug}/2`);
|
||||
|
||||
const acceptCookies = page.getByRole('button', { name: /akzeptieren|accept/i });
|
||||
if (await acceptCookies.isVisible()) {
|
||||
await acceptCookies.click();
|
||||
}
|
||||
await dismissConsentBanner(page);
|
||||
|
||||
// If login/register step is present, choose guest path or continue
|
||||
const continueButtons = page.getByRole('button', { name: /weiter|continue/i });
|
||||
@@ -25,20 +32,10 @@ test.describe('Paddle sandbox full flow (staging)', () => {
|
||||
await continueButtons.first().click();
|
||||
}
|
||||
|
||||
// Fill minimal registration form to reach payment step
|
||||
await page.fill('input[name="first_name"]', 'Play');
|
||||
await page.fill('input[name="last_name"]', 'Wright');
|
||||
await page.fill('input[name="email"]', sandboxEmail);
|
||||
await page.fill('input[name="address"]', 'Teststrasse 1, 12345 Berlin');
|
||||
await page.fill('input[name="phone"]', '+49123456789');
|
||||
await page.fill('input[name="username"]', 'playwright-buyer');
|
||||
await page.fill('input[name="password"]', 'Password123!');
|
||||
await page.fill('input[name="password_confirmation"]', 'Password123!');
|
||||
|
||||
await page.check('input[name="privacy_consent"]');
|
||||
await page.getByRole('button', { name: /^Registrieren$/i }).last().click();
|
||||
|
||||
await expect(page.getByPlaceholder(/Gutscheincode/i)).toBeVisible({ timeout: 20000 });
|
||||
await completeRegistrationOrLogin(page, {
|
||||
email: tenantEmail!,
|
||||
password: tenantPassword!,
|
||||
});
|
||||
|
||||
const termsCheckbox = page.locator('#checkout-terms-hero');
|
||||
await expect(termsCheckbox).toBeVisible();
|
||||
@@ -57,34 +54,186 @@ test.describe('Paddle sandbox full flow (staging)', () => {
|
||||
|
||||
expect(checkoutUrl).toContain('paddle');
|
||||
|
||||
// Navigate to checkout to ensure it loads (hosted page). Use sandbox card data if needed later.
|
||||
// Navigate to Paddle hosted checkout and complete payment.
|
||||
await page.goto(checkoutUrl);
|
||||
await expect(page).toHaveURL(/paddle/);
|
||||
|
||||
// Fetch latest session for this buyer
|
||||
const latestSession = await request.get('/api/_testing/checkout/sessions/latest', {
|
||||
params: { email: sandboxEmail },
|
||||
});
|
||||
await completeHostedPaddleCheckout(page, sandboxCard);
|
||||
|
||||
expect(latestSession.status()).toBe(200);
|
||||
const sessionJson = await latestSession.json();
|
||||
const sessionId: string | undefined = sessionJson?.data?.id;
|
||||
expect(sessionId, 'checkout session id').toBeTruthy();
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const latestCompleted = await request.get('/api/_testing/checkout/sessions/latest', {
|
||||
params: { status: 'completed', email: tenantEmail },
|
||||
});
|
||||
if (!latestCompleted.ok()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simulate Paddle webhook completion
|
||||
const simulate = await request.post(`/api/_testing/checkout/sessions/${sessionId}/simulate-paddle`, {
|
||||
data: {
|
||||
status: 'completed',
|
||||
transaction_id: 'txn_playwright_' + Date.now(),
|
||||
},
|
||||
});
|
||||
const json = await latestCompleted.json();
|
||||
return json?.data?.status ?? null;
|
||||
},
|
||||
{ timeout: 120_000 }
|
||||
)
|
||||
.toBe('completed');
|
||||
|
||||
expect(simulate.status()).toBe(200);
|
||||
await page.goto(`${baseUrl}/event-admin/login`);
|
||||
await dismissConsentBanner(page);
|
||||
|
||||
// Confirm session is marked completed
|
||||
const latestCompleted = await request.get('/api/_testing/checkout/sessions/latest', {
|
||||
params: { status: 'completed', email: sandboxEmail },
|
||||
});
|
||||
expect(latestCompleted.status()).toBe(200);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
await page.fill('input[name="address"]', 'Teststrasse 1, 12345 Berlin');
|
||||
await page.fill('input[name="phone"]', '+49123456789');
|
||||
|
||||
const username = credentials.email.split('@')[0]?.replace(/[^a-z0-9]+/gi, '-') ?? `playwright-${Date.now()}`;
|
||||
await page.fill('input[name="username"]', username);
|
||||
|
||||
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 completeHostedPaddleCheckout(
|
||||
page: Page,
|
||||
card: { number: string; expiry: string; cvc: string; name: string; postal: string }
|
||||
): Promise<void> {
|
||||
const cardNumberSelectors = [
|
||||
'input[autocomplete="cc-number"]',
|
||||
'input[name="cardnumber"]',
|
||||
'input[name="card_number"]',
|
||||
];
|
||||
const expirySelectors = [
|
||||
'input[autocomplete="cc-exp"]',
|
||||
'input[name="exp-date"]',
|
||||
'input[name="exp_date"]',
|
||||
'input[name="expiry"]',
|
||||
];
|
||||
const cvcSelectors = [
|
||||
'input[autocomplete="cc-csc"]',
|
||||
'input[name="cvc"]',
|
||||
'input[name="security-code"]',
|
||||
'input[name="cvv"]',
|
||||
];
|
||||
const nameSelectors = [
|
||||
'input[autocomplete="cc-name"]',
|
||||
'input[name="cardholder"]',
|
||||
'input[name="cardholder_name"]',
|
||||
'input[name="cardholder-name"]',
|
||||
];
|
||||
const postalSelectors = [
|
||||
'input[autocomplete="postal-code"]',
|
||||
'input[name="postal"]',
|
||||
'input[name="postal_code"]',
|
||||
'input[name="zip"]',
|
||||
];
|
||||
|
||||
await maybeFillInAnyFrame(page, nameSelectors, card.name);
|
||||
await fillInAnyFrame(page, cardNumberSelectors, card.number);
|
||||
await fillInAnyFrame(page, expirySelectors, card.expiry);
|
||||
await fillInAnyFrame(page, cvcSelectors, card.cvc);
|
||||
await maybeFillInAnyFrame(page, postalSelectors, card.postal);
|
||||
|
||||
const payButton = page.getByRole('button', {
|
||||
name: /Jetzt bezahlen|Pay now|Complete order|Order now|Kaufen|Bezahlen|Zahlung abschließen/i,
|
||||
}).first();
|
||||
await expect(payButton).toBeVisible({ timeout: 20_000 });
|
||||
await payButton.click();
|
||||
}
|
||||
|
||||
async function fillInAnyFrame(page: Page, selectors: string[], value: string): Promise<void> {
|
||||
const filled = await attemptFill(page, selectors, value);
|
||||
if (filled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frames = page.frames();
|
||||
for (const frame of frames) {
|
||||
const frameFilled = await attemptFill(frame, selectors, value);
|
||||
if (frameFilled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to find input for selectors: ${selectors.join(', ')}`);
|
||||
}
|
||||
|
||||
async function maybeFillInAnyFrame(page: Page, selectors: string[], value: string): Promise<void> {
|
||||
const filled = await attemptFill(page, selectors, value);
|
||||
if (filled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frames = page.frames();
|
||||
for (const frame of frames) {
|
||||
const frameFilled = await attemptFill(frame, selectors, value);
|
||||
if (frameFilled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function attemptFill(
|
||||
scope: Page | import('@playwright/test').Frame,
|
||||
selectors: string[],
|
||||
value: string
|
||||
): Promise<boolean> {
|
||||
for (const selector of selectors) {
|
||||
const locator = scope.locator(selector).first();
|
||||
if ((await locator.count()) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await locator.fill(value);
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
await locator.click();
|
||||
await locator.type(value, { delay: 25 });
|
||||
return true;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user