Testing passwordless flows
Testing
You can test Kinde authentication flows using browser automation tools like Playwright, Cypress, and AWS CloudWatch Synthetic Canaries.
Different browser automation tools offer various trade-offs. Here’s a comparison:
| Method | Pros | Cons |
|---|---|---|
| AWS Synthetic Canaries | Native AWS integration, scheduled runs, built-in metrics | Higher cost per execution |
| Playwright (self-hosted) | Modern API, cross-browser support, free | Requires CI/CD setup |
| Cypress Cloud | Great DX, parallel execution | Subscription cost |
| Self-hosted Puppeteer | Lower cost, full control | Requires infrastructure management |
Kinde requires OTP email verification when signing up for a new user. See the Testing passwordless flows guide for details on how to handle OTP verification in your test methods.
The typical sign-up flow involves:
input[name="p_first_name"]input[name="p_last_name"]input[name="p_email"]input[name="p_first_password"]input[name="p_second_password"]input[name="p_has_clickwrap_accepted"]input[name="p_confirmation_code"]button[type="submit"]test-${timestamp}@example.com)The typical sign-in flow involves:
The typical sign-out flow involves:
Here’s a pattern using AWS CloudWatch Synthetic Canaries with Puppeteer:
// Synthetic canary scriptconst synthetics = require('Synthetics');
const flowStep = async (stepName, action) => { await synthetics.executeStep(stepName, action);};
exports.handler = async () => { const page = await synthetics.getPage();
// Store testEmail outside flowStep for OTP handling let testEmail;
// Sign-up flow await flowStep('Open app for sign-up', async () => { await page.goto(process.env.TEST_APP_URL, { waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Select sign up', async () => { await page.waitForSelector('[data-testid="sign-up-button"]', { timeout: 10000 }); await page.click('[data-testid="sign-up-button"]'); // Wait for navigation to Kinde await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Fill sign-up form', async () => { const timestamp = Date.now(); testEmail = `test-${timestamp}@testemail.com`;
await page.waitForSelector('input[name="p_first_name"]', { timeout: 10000 }); await page.type('input[name="p_first_name"]', 'John'); await page.type('input[name="p_last_name"]', 'Doe'); await page.type('input[name="p_email"]', testEmail); await page.click('input[name="p_has_clickwrap_accepted"]'); // Use click() instead of check() await page.click('button[type="submit"]'); // Wait for OTP page await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Handle OTP verification', async () => { // Handle OTP email verification in this step // See Testing Passwordless Flows guide for email service integration // Example: const otpCode = await getOTPFromEmail(testEmail); // await page.type('input[name="p_confirmation_code"]', otpCode); // await page.click('button[type="submit"]'); // await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Create password', async () => { await page.waitForSelector('input[name="p_first_password"]', { timeout: 10000 }); await page.type('input[name="p_first_password"]', process.env.TEST_USER_PASSWORD); await page.type('input[name="p_second_password"]', process.env.TEST_USER_PASSWORD); await page.click('button[type="submit"]'); // Wait for redirect back to app await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Verify signed up', async () => { await page.waitForSelector('[data-testid="user-profile"]', { timeout: 10000 }); });
// Sign-in flow (sign out first to reset state) await flowStep('Sign out before sign-in test', async () => { await page.waitForSelector('[data-testid="sign-out-button"]', { timeout: 10000 }); await page.click('[data-testid="sign-out-button"]'); await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Select sign in', async () => { await page.waitForSelector('[data-testid="sign-in-button"]', { timeout: 10000 }); await page.click('[data-testid="sign-in-button"]'); await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Enter email', async () => { await page.waitForSelector('input[name="p_email"]', { timeout: 10000 }); await page.type('input[name="p_email"]', process.env.TEST_USER_EMAIL); await page.click('button[type="submit"]'); await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Enter password', async () => { await page.waitForSelector('input[name="p_password"]', { timeout: 10000 }); await page.type('input[name="p_password"]', process.env.TEST_USER_PASSWORD); await page.click('button[type="submit"]'); await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Verify signed in', async () => { await page.waitForSelector('[data-testid="user-profile"]', { timeout: 10000 }); });
// Sign-out flow await flowStep('Sign out', async () => { await page.waitForSelector('[data-testid="sign-out-button"]', { timeout: 10000 }); await page.click('[data-testid="sign-out-button"]'); await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }); });
await flowStep('Verify signed out', async () => { await page.waitForSelector('[data-testid="sign-in-button"]', { timeout: 10000 }); });};The following limitations apply to UI authentication flow testing:
Test authentication flows with: