432 lines
18 KiB
TypeScript
432 lines
18 KiB
TypeScript
|
|
import { test, expect } from '@playwright/test';
|
|||
|
|
import { acceptCookieConsent, waitForNavigation } from '../helpers/utils';
|
|||
|
|
import path from 'path';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Complete User Registration and Onboarding Flow Test
|
|||
|
|
*
|
|||
|
|
* This test suite covers the entire journey from registration to onboarding completion:
|
|||
|
|
* 1. User Registration (3-step process: basic info, subscription, payment)
|
|||
|
|
* 2. Onboarding Wizard (multiple steps based on business type and tier)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
test.describe('Complete Registration and Onboarding Flow', () => {
|
|||
|
|
// Generate unique test user for each test run
|
|||
|
|
const generateTestUser = () => ({
|
|||
|
|
fullName: `Test User ${Date.now()}`,
|
|||
|
|
email: `test.user.${Date.now()}@bakery-test.com`,
|
|||
|
|
password: 'SecurePass123!@#',
|
|||
|
|
plan: 'starter' // or 'professional', 'enterprise'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('should complete full registration and onboarding flow for starter plan', async ({ page }) => {
|
|||
|
|
const testUser = generateTestUser();
|
|||
|
|
|
|||
|
|
// ============================================
|
|||
|
|
// PHASE 1: REGISTRATION
|
|||
|
|
// ============================================
|
|||
|
|
|
|||
|
|
await test.step('Navigate to registration page', async () => {
|
|||
|
|
await page.goto('/register');
|
|||
|
|
await acceptCookieConsent(page);
|
|||
|
|
|
|||
|
|
// Verify we're on the registration page
|
|||
|
|
await expect(page.getByRole('heading', { name: /crear cuenta|create account/i })).toBeVisible();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Fill basic information (Step 1/3)', async () => {
|
|||
|
|
// Fill in user details
|
|||
|
|
await page.getByLabel(/nombre completo|full name/i).fill(testUser.fullName);
|
|||
|
|
await page.getByLabel(/correo|email/i).fill(testUser.email);
|
|||
|
|
|
|||
|
|
// Fill password
|
|||
|
|
await page.getByLabel(/^contraseña|^password/i).first().fill(testUser.password);
|
|||
|
|
|
|||
|
|
// Fill confirm password
|
|||
|
|
await page.getByLabel(/confirmar contraseña|confirm password/i).fill(testUser.password);
|
|||
|
|
|
|||
|
|
// Wait for password match indicator
|
|||
|
|
await expect(page.getByText(/las contraseñas coinciden|passwords match/i)).toBeVisible({ timeout: 5000 });
|
|||
|
|
|
|||
|
|
// Accept terms and conditions
|
|||
|
|
await page.getByRole('checkbox', { name: /acepto los términos|accept terms/i }).check();
|
|||
|
|
|
|||
|
|
// Optional consents
|
|||
|
|
await page.getByRole('checkbox', { name: /marketing|promociones/i }).check();
|
|||
|
|
await page.getByRole('checkbox', { name: /analytics|analíticas/i }).check();
|
|||
|
|
|
|||
|
|
// Proceed to next step
|
|||
|
|
await page.getByRole('button', { name: /siguiente|next/i }).click();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Select subscription plan (Step 2/3)', async () => {
|
|||
|
|
// Wait for subscription page
|
|||
|
|
await expect(page.getByRole('heading', { name: /selecciona tu plan|select.*plan/i })).toBeVisible({ timeout: 10000 });
|
|||
|
|
|
|||
|
|
// Select starter plan (default is usually selected)
|
|||
|
|
const starterPlanButton = page.getByRole('button', { name: /starter|básico/i }).first();
|
|||
|
|
if (await starterPlanButton.isVisible().catch(() => false)) {
|
|||
|
|
await starterPlanButton.click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Proceed to payment
|
|||
|
|
await page.getByRole('button', { name: /siguiente|next/i }).click();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Complete payment information (Step 3/3)', async () => {
|
|||
|
|
// Wait for payment page
|
|||
|
|
await expect(page.getByRole('heading', { name: /información de pago|payment info/i })).toBeVisible({ timeout: 10000 });
|
|||
|
|
|
|||
|
|
// Check if bypass payment toggle exists (for testing environments)
|
|||
|
|
const bypassToggle = page.getByRole('checkbox', { name: /bypass|omitir|skip.*payment/i });
|
|||
|
|
const hasBypassOption = await bypassToggle.isVisible({ timeout: 2000 }).catch(() => false);
|
|||
|
|
|
|||
|
|
if (hasBypassOption) {
|
|||
|
|
// Enable bypass for test environment
|
|||
|
|
await bypassToggle.check();
|
|||
|
|
await page.getByRole('button', { name: /completar registro|complete registration/i }).click();
|
|||
|
|
} else {
|
|||
|
|
// Fill Stripe test card details
|
|||
|
|
const cardFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first();
|
|||
|
|
|
|||
|
|
// Wait for Stripe iframe to load
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
|
|||
|
|
// Fill card number (Stripe test card)
|
|||
|
|
await cardFrame.getByPlaceholder(/card number|número.*tarjeta/i).fill('4242424242424242');
|
|||
|
|
|
|||
|
|
// Fill expiry date
|
|||
|
|
await cardFrame.getByPlaceholder(/mm.*yy|expir/i).fill('1234');
|
|||
|
|
|
|||
|
|
// Fill CVC
|
|||
|
|
await cardFrame.getByPlaceholder(/cvc|cvv/i).fill('123');
|
|||
|
|
|
|||
|
|
// Fill postal code if visible
|
|||
|
|
const postalCode = cardFrame.getByPlaceholder(/postal|zip/i);
|
|||
|
|
if (await postalCode.isVisible().catch(() => false)) {
|
|||
|
|
await postalCode.fill('12345');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Submit payment
|
|||
|
|
await page.getByRole('button', { name: /completar registro|complete registration|submit/i }).click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait for redirect to onboarding
|
|||
|
|
await page.waitForURL(/\/app\/onboarding/, { timeout: 15000 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ============================================
|
|||
|
|
// PHASE 2: ONBOARDING WIZARD
|
|||
|
|
// ============================================
|
|||
|
|
|
|||
|
|
await test.step('Complete onboarding wizard', async () => {
|
|||
|
|
// Wait for onboarding page to load
|
|||
|
|
await expect(page.locator('body')).toContainText(/onboarding|configuración|bienvenido/i, { timeout: 10000 });
|
|||
|
|
|
|||
|
|
// The onboarding has multiple steps, we'll navigate through them
|
|||
|
|
let stepCount = 0;
|
|||
|
|
const maxSteps = 15; // Maximum expected steps
|
|||
|
|
|
|||
|
|
while (stepCount < maxSteps) {
|
|||
|
|
// Wait a bit for the step to render
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// Look for various button types that indicate progression
|
|||
|
|
const nextButton = page.getByRole('button', { name: /next|siguiente|continuar/i });
|
|||
|
|
const skipButton = page.getByRole('button', { name: /skip|omitir|saltar/i });
|
|||
|
|
const finishButton = page.getByRole('button', { name: /finish|finalizar|completar/i });
|
|||
|
|
const goToDashboardButton = page.getByRole('button', { name: /ir al panel|go to dashboard/i });
|
|||
|
|
|
|||
|
|
// Check what step we're on and fill accordingly
|
|||
|
|
const bodyText = await page.locator('body').textContent();
|
|||
|
|
|
|||
|
|
// Step: Bakery Type Selection
|
|||
|
|
if (bodyText?.match(/tipo.*panadería|bakery.*type|selecciona.*tipo/i)) {
|
|||
|
|
console.log('📍 Step: Bakery Type Selection');
|
|||
|
|
const retailOption = page.getByRole('button', { name: /retail|minorista|venta/i }).first();
|
|||
|
|
if (await retailOption.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await retailOption.click();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: Tenant Setup (Register Bakery)
|
|||
|
|
else if (bodyText?.match(/registra.*panadería|register.*bakery|datos.*negocio/i)) {
|
|||
|
|
console.log('📍 Step: Tenant Setup');
|
|||
|
|
|
|||
|
|
// Fill bakery name if not filled
|
|||
|
|
const nameInput = page.getByLabel(/nombre.*panadería|bakery.*name|nombre.*negocio/i);
|
|||
|
|
if (await nameInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
const currentValue = await nameInput.inputValue();
|
|||
|
|
if (!currentValue) {
|
|||
|
|
await nameInput.fill(`Test Bakery ${Date.now()}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fill address
|
|||
|
|
const addressInput = page.getByLabel(/dirección|address/i);
|
|||
|
|
if (await addressInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
const currentValue = await addressInput.inputValue();
|
|||
|
|
if (!currentValue) {
|
|||
|
|
await addressInput.fill('Calle Test 123');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fill postal code
|
|||
|
|
const postalInput = page.getByLabel(/código postal|postal.*code|zip/i);
|
|||
|
|
if (await postalInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
const currentValue = await postalInput.inputValue();
|
|||
|
|
if (!currentValue) {
|
|||
|
|
await postalInput.fill('28001');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fill city
|
|||
|
|
const cityInput = page.getByLabel(/ciudad|city/i);
|
|||
|
|
if (await cityInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
const currentValue = await cityInput.inputValue();
|
|||
|
|
if (!currentValue) {
|
|||
|
|
await cityInput.fill('Madrid');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fill phone
|
|||
|
|
const phoneInput = page.getByLabel(/teléfono|phone/i);
|
|||
|
|
if (await phoneInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
const currentValue = await phoneInput.inputValue();
|
|||
|
|
if (!currentValue) {
|
|||
|
|
await phoneInput.fill('666555444');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: Upload Sales Data
|
|||
|
|
else if (bodyText?.match(/subir.*datos|upload.*sales|archivo.*ventas/i)) {
|
|||
|
|
console.log('📍 Step: Upload Sales Data');
|
|||
|
|
|
|||
|
|
// Try to skip this step if possible
|
|||
|
|
if (await skipButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await skipButton.click();
|
|||
|
|
stepCount++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Otherwise, we'd need to upload a test CSV file
|
|||
|
|
// For now, we'll just proceed if there's a next button
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: Inventory Review
|
|||
|
|
else if (bodyText?.match(/revisar.*inventario|inventory.*review|productos/i)) {
|
|||
|
|
console.log('📍 Step: Inventory Review');
|
|||
|
|
// Usually can skip or proceed with empty
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: Initial Stock Entry
|
|||
|
|
else if (bodyText?.match(/stock.*inicial|initial.*stock|cantidad/i)) {
|
|||
|
|
console.log('📍 Step: Initial Stock Entry');
|
|||
|
|
|
|||
|
|
// Look for "Set all to 0" or skip button
|
|||
|
|
const setAllZeroButton = page.getByRole('button', { name: /establecer.*0|set.*all.*0/i });
|
|||
|
|
if (await setAllZeroButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await setAllZeroButton.click();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: Suppliers Setup
|
|||
|
|
else if (bodyText?.match(/proveedores|suppliers/i)) {
|
|||
|
|
console.log('📍 Step: Suppliers Setup');
|
|||
|
|
// Can skip for now
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: Recipes Setup
|
|||
|
|
else if (bodyText?.match(/recetas|recipes/i)) {
|
|||
|
|
console.log('📍 Step: Recipes Setup');
|
|||
|
|
// Can skip for now
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: ML Training
|
|||
|
|
else if (bodyText?.match(/entrenamiento|training|modelo.*ia|ai.*model/i)) {
|
|||
|
|
console.log('📍 Step: ML Training');
|
|||
|
|
|
|||
|
|
// Look for skip to dashboard button
|
|||
|
|
if (await goToDashboardButton.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|||
|
|
await goToDashboardButton.click();
|
|||
|
|
// Should redirect to dashboard
|
|||
|
|
await page.waitForURL(/\/app\/(dashboard|operations)/, { timeout: 10000 });
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Step: Completion
|
|||
|
|
else if (bodyText?.match(/completado|completed|felicidades|congratulations/i)) {
|
|||
|
|
console.log('📍 Step: Completion');
|
|||
|
|
|
|||
|
|
if (await goToDashboardButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await goToDashboardButton.click();
|
|||
|
|
await page.waitForURL(/\/app\/(dashboard|operations)/, { timeout: 10000 });
|
|||
|
|
break;
|
|||
|
|
} else if (await finishButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await finishButton.click();
|
|||
|
|
await page.waitForURL(/\/app\/(dashboard|operations)/, { timeout: 10000 });
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Try to proceed to next step
|
|||
|
|
if (await nextButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await nextButton.click();
|
|||
|
|
stepCount++;
|
|||
|
|
} else if (await skipButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await skipButton.click();
|
|||
|
|
stepCount++;
|
|||
|
|
} else if (await finishButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await finishButton.click();
|
|||
|
|
// Should redirect to dashboard
|
|||
|
|
await page.waitForURL(/\/app\/(dashboard|operations)/, { timeout: 10000 });
|
|||
|
|
break;
|
|||
|
|
} else {
|
|||
|
|
// No more navigation buttons found
|
|||
|
|
console.log('ℹ️ No more navigation buttons found, checking if we reached dashboard');
|
|||
|
|
|
|||
|
|
// Check if we're already at dashboard
|
|||
|
|
if (page.url().includes('/app/dashboard') || page.url().includes('/app/operations')) {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// If we can't find any buttons and we're still on onboarding, something might be wrong
|
|||
|
|
if (page.url().includes('/onboarding')) {
|
|||
|
|
console.warn('⚠️ Still on onboarding page but no navigation buttons found');
|
|||
|
|
// Take a screenshot for debugging
|
|||
|
|
await page.screenshot({ path: `test-results/onboarding-stuck-step-${stepCount}.png` });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Safety check: wait a bit between steps
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`✅ Completed ${stepCount} onboarding steps`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Verify successful onboarding completion', async () => {
|
|||
|
|
// Should be redirected to dashboard
|
|||
|
|
await expect(page).toHaveURL(/\/app\/(dashboard|operations)/, { timeout: 10000 });
|
|||
|
|
|
|||
|
|
// Verify dashboard elements are visible
|
|||
|
|
await expect(page.locator('body')).toContainText(/dashboard|panel|operaciones|operations/i, { timeout: 5000 });
|
|||
|
|
|
|||
|
|
// Take final screenshot
|
|||
|
|
await page.screenshot({ path: 'test-results/onboarding-completed.png', fullPage: true });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('should complete onboarding with manual data entry (no file upload)', async ({ page }) => {
|
|||
|
|
const testUser = generateTestUser();
|
|||
|
|
|
|||
|
|
// Quick registration (simplified for this test)
|
|||
|
|
await page.goto('/register');
|
|||
|
|
await acceptCookieConsent(page);
|
|||
|
|
|
|||
|
|
// Fill registration form quickly
|
|||
|
|
await page.getByLabel(/nombre completo|full name/i).fill(testUser.fullName);
|
|||
|
|
await page.getByLabel(/correo|email/i).fill(testUser.email);
|
|||
|
|
await page.getByLabel(/^contraseña|^password/i).first().fill(testUser.password);
|
|||
|
|
await page.getByLabel(/confirmar contraseña|confirm password/i).fill(testUser.password);
|
|||
|
|
await page.getByRole('checkbox', { name: /acepto los términos|accept terms/i }).check();
|
|||
|
|
|
|||
|
|
// Proceed through registration
|
|||
|
|
await page.getByRole('button', { name: /siguiente|next/i }).click();
|
|||
|
|
|
|||
|
|
// Select plan (if subscription step appears)
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
const nextButtonAfterPlan = page.getByRole('button', { name: /siguiente|next/i });
|
|||
|
|
if (await nextButtonAfterPlan.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|||
|
|
await nextButtonAfterPlan.click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Handle payment page
|
|||
|
|
const bypassToggle = page.getByRole('checkbox', { name: /bypass|omitir|skip.*payment/i });
|
|||
|
|
if (await bypassToggle.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|||
|
|
await bypassToggle.check();
|
|||
|
|
await page.getByRole('button', { name: /completar registro|complete registration/i }).click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait for onboarding
|
|||
|
|
await page.waitForURL(/\/app\/onboarding/, { timeout: 15000 });
|
|||
|
|
|
|||
|
|
// Test specific scenario: Skip file upload and add products manually
|
|||
|
|
await test.step('Navigate to manual product entry', async () => {
|
|||
|
|
// Look for file upload step and skip it
|
|||
|
|
let attempts = 0;
|
|||
|
|
while (attempts < 10) {
|
|||
|
|
const bodyText = await page.locator('body').textContent();
|
|||
|
|
|
|||
|
|
if (bodyText?.match(/subir.*archivo|upload.*file|sales.*data/i)) {
|
|||
|
|
// Found upload step, try to skip
|
|||
|
|
const skipButton = page.getByRole('button', { name: /skip|omitir|saltar|manual/i });
|
|||
|
|
if (await skipButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await skipButton.click();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Try next button if available
|
|||
|
|
const nextButton = page.getByRole('button', { name: /next|siguiente/i });
|
|||
|
|
if (await nextButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|||
|
|
await nextButton.click();
|
|||
|
|
attempts++;
|
|||
|
|
} else {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('should handle validation errors gracefully', async ({ page }) => {
|
|||
|
|
await page.goto('/register');
|
|||
|
|
await acceptCookieConsent(page);
|
|||
|
|
|
|||
|
|
await test.step('Test password validation', async () => {
|
|||
|
|
// Try weak password
|
|||
|
|
await page.getByLabel(/nombre completo|full name/i).fill('Test User');
|
|||
|
|
await page.getByLabel(/correo|email/i).fill('test@example.com');
|
|||
|
|
await page.getByLabel(/^contraseña|^password/i).first().fill('weak');
|
|||
|
|
|
|||
|
|
// Next button should be disabled or show error
|
|||
|
|
const nextButton = page.getByRole('button', { name: /siguiente|next/i });
|
|||
|
|
|
|||
|
|
// Either button is disabled or we see password criteria errors
|
|||
|
|
const isDisabled = await nextButton.isDisabled();
|
|||
|
|
const hasPasswordError = await page.getByText(/mínimo|caracteres|mayúscula|minúscula/i).isVisible().catch(() => false);
|
|||
|
|
|
|||
|
|
expect(isDisabled || hasPasswordError).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Test email validation', async () => {
|
|||
|
|
// Try invalid email
|
|||
|
|
await page.getByLabel(/correo|email/i).fill('invalid-email');
|
|||
|
|
await page.getByLabel(/^contraseña|^password/i).first().click(); // Blur the email field
|
|||
|
|
|
|||
|
|
// Should show email error
|
|||
|
|
await expect(page.getByText(/email.*válido|valid.*email/i)).toBeVisible({ timeout: 3000 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await test.step('Test terms acceptance requirement', async () => {
|
|||
|
|
// Fill valid data but don't accept terms
|
|||
|
|
await page.getByLabel(/nombre completo|full name/i).fill('Test User');
|
|||
|
|
await page.getByLabel(/correo|email/i).fill('test@example.com');
|
|||
|
|
await page.getByLabel(/^contraseña|^password/i).first().fill('SecurePass123!');
|
|||
|
|
await page.getByLabel(/confirmar contraseña|confirm password/i).fill('SecurePass123!');
|
|||
|
|
|
|||
|
|
// Try to proceed without accepting terms
|
|||
|
|
const nextButton = page.getByRole('button', { name: /siguiente|next/i });
|
|||
|
|
|
|||
|
|
// Button should be disabled
|
|||
|
|
await expect(nextButton).toBeDisabled();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|