mirror of
https://github.com/Mail-0/Zero.git
synced 2026-03-03 03:47:01 +00:00
feat: add tests using playwright (#1877)
1. Get both the better auth session tokens from appliations/cookies in .env <img width="852" height="316" alt="image" src="https://github.com/user-attachments/assets/0177c496-103c-4111-8a80-089d1f4a6f94" /> 2. Enter the email you wish to send to in .env 3. `cd packages/testing` 3. run `npm test:e2e:headed` thats it tbh https://github.com/user-attachments/assets/b703e78c-2373-40a2-b431-f9ba53d5d871 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Added Playwright end-to-end tests for the mail inbox flow, including authentication setup and email send/reply actions. - **New Features** - Added Playwright config, test scripts, and environment variables for E2E testing. - Implemented tests to sign in, send an email, and reply within the same session. <!-- End of auto-generated description by cubic. --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Introduced a comprehensive testing package with support for unit, UI, and end-to-end tests. * Added Playwright-based authentication setup and mail inbox end-to-end test scripts. * Provided a dedicated test configuration and TypeScript setup for robust test execution. * **Chores** * Updated environment variable examples to support Playwright testing. * Enhanced main project scripts to facilitate various testing modes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -37,4 +37,9 @@ AUTUMN_SECRET_KEY=
|
||||
|
||||
TWILIO_ACCOUNT_SID=
|
||||
TWILIO_AUTH_TOKEN=
|
||||
TWILIO_PHONE_NUMBER=
|
||||
TWILIO_PHONE_NUMBER=
|
||||
|
||||
# FOR PLAYWRIGHT E2E TESTING
|
||||
PLAYWRIGHT_SESSION_TOKEN =
|
||||
PLAYWRIGHT_SESSION_DATA =
|
||||
EMAIL =
|
||||
@@ -29,6 +29,10 @@
|
||||
"db:studio": "dotenv -- pnpm run -C apps/server db:studio",
|
||||
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org zero-7y --project nextjs ./apps/mail/.next && sentry-cli sourcemaps upload --org zero-7y --project nextjs ./apps/mail/.next",
|
||||
"scripts": "dotenv -- pnpx tsx ./scripts/run.ts",
|
||||
"test": "pnpm --filter=@zero/testing test",
|
||||
"test:watch": "pnpm --filter=@zero/testing test:watch",
|
||||
"test:coverage": "pnpm --filter=@zero/testing test:coverage",
|
||||
"test:ui": "pnpm --filter=@zero/testing test:ui",
|
||||
"test:ai": "dotenv -- pnpm --filter=@zero/server run test:ai",
|
||||
"eval": "dotenv -- pnpm --filter=@zero/server run eval",
|
||||
"eval:dev": "dotenv -- pnpm --filter=@zero/server run eval:dev",
|
||||
|
||||
77
packages/testing/e2e/auth.setup.ts
Normal file
77
packages/testing/e2e/auth.setup.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { test as setup } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
|
||||
|
||||
setup('inject real authentication session', async ({ page }) => {
|
||||
console.log('Injecting real authentication session...');
|
||||
|
||||
const SessionToken = process.env.PLAYWRIGHT_SESSION_TOKEN;
|
||||
const SessionData = process.env.PLAYWRIGHT_SESSION_DATA;
|
||||
|
||||
if (!SessionToken || !SessionData) {
|
||||
throw new Error('PLAYWRIGHT_SESSION_TOKEN and PLAYWRIGHT_SESSION_DATA environment variables must be set.');
|
||||
}
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
|
||||
console.log('Page loaded, setting up authentication...');
|
||||
|
||||
// sets better auth session cookies
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'better-auth-dev.session_token',
|
||||
value: SessionToken,
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax'
|
||||
},
|
||||
{
|
||||
name: 'better-auth-dev.session_data',
|
||||
value: SessionData,
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax'
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('Real session cookies injected');
|
||||
|
||||
try {
|
||||
const decodedSessionData = JSON.parse(atob(SessionData));
|
||||
|
||||
await page.addInitScript((sessionData) => {
|
||||
if (sessionData.session) {
|
||||
localStorage.setItem('better-auth.session', JSON.stringify(sessionData.session.session));
|
||||
localStorage.setItem('better-auth.user', JSON.stringify(sessionData.session.user));
|
||||
}
|
||||
}, decodedSessionData);
|
||||
|
||||
console.log('Session data set in localStorage');
|
||||
} catch (error) {
|
||||
console.log('Could not decode session data for localStorage:', error);
|
||||
}
|
||||
|
||||
await page.goto('/mail/inbox');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
const currentUrl = page.url();
|
||||
console.log('Current URL after clicking Get Started:', currentUrl);
|
||||
|
||||
if (currentUrl.includes('/mail')) {
|
||||
console.log('Successfully reached mail app! On:', currentUrl);
|
||||
} else {
|
||||
console.log('Did not reach mail app. Current URL:', currentUrl);
|
||||
await page.screenshot({ path: 'debug-auth-failed.png' });
|
||||
}
|
||||
|
||||
await page.context().storageState({ path: authFile });
|
||||
|
||||
console.log('Real authentication session injected and saved!');
|
||||
});
|
||||
86
packages/testing/e2e/mail-inbox.spec.ts
Normal file
86
packages/testing/e2e/mail-inbox.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const email = process.env.EMAIL;
|
||||
|
||||
if (!email) {
|
||||
throw new Error('EMAIL environment variable must be set.');
|
||||
}
|
||||
|
||||
test.describe('Signing In, Sending mail, Replying to a mail', () => {
|
||||
test('should send and reply to an email in the same session', async ({ page }) => {
|
||||
await page.goto('/mail/inbox');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
console.log('Successfully accessed mail inbox');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
try {
|
||||
const welcomeModal = page.getByText('Welcome to Zero Email!');
|
||||
if (await welcomeModal.isVisible({ timeout: 2000 })) {
|
||||
console.log('Onboarding modal detected, clicking outside to dismiss...');
|
||||
await page.locator('body').click({ position: { x: 100, y: 100 } });
|
||||
await page.waitForTimeout(1500);
|
||||
console.log('Modal successfully dismissed');
|
||||
}
|
||||
} catch {
|
||||
console.log('No onboarding modal found, proceeding...');
|
||||
}
|
||||
|
||||
await expect(page.getByText('Inbox')).toBeVisible();
|
||||
console.log('Mail inbox is now visible');
|
||||
|
||||
console.log('Starting email sending process...');
|
||||
await page.getByText('New email').click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.locator('input').first().fill(email);
|
||||
console.log('Filled To: field');
|
||||
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
console.log('Clicked Send button');
|
||||
await page.waitForTimeout(3000);
|
||||
console.log('Email sent successfully!');
|
||||
|
||||
console.log('Waiting for email to arrive...');
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
console.log('Looking for the first email in the list...');
|
||||
await page.locator('[data-thread-id]').first().click();
|
||||
console.log('Clicked on email (PM/AM area).');
|
||||
|
||||
console.log('Looking for Reply button to confirm email is open...');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const replySelectors = [
|
||||
'button:has-text("Reply")',
|
||||
'[data-testid*="reply"]',
|
||||
'button[title*="Reply"]',
|
||||
'button:text-is("Reply")',
|
||||
'button:text("Reply")'
|
||||
];
|
||||
|
||||
let replyClicked = false;
|
||||
for (const selector of replySelectors) {
|
||||
try {
|
||||
await page.locator(selector).first().click({ force: true });
|
||||
console.log(`Clicked Reply button using: ${selector}`);
|
||||
replyClicked = true;
|
||||
break;
|
||||
} catch {
|
||||
console.log(`Failed to click with ${selector}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!replyClicked) {
|
||||
console.log('Could not find Reply button');
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('Sending reply...');
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
await page.waitForTimeout(3000);
|
||||
console.log('Reply sent successfully!');
|
||||
|
||||
console.log('Entire email flow completed successfully!');
|
||||
});
|
||||
});
|
||||
38
packages/testing/package.json
Normal file
38
packages/testing/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@zero/testing",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:headed": "playwright test --headed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/playwright": "0.0.11",
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/testing-library__jest-dom": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/coverage-v8": "^1.0.4",
|
||||
"@vitest/ui": "^1.0.4",
|
||||
"happy-dom": "^12.10.3",
|
||||
"jsdom": "^23.0.1",
|
||||
"msw": "^2.0.8",
|
||||
"dotenv": "^16.3.1",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.81.5"
|
||||
}
|
||||
}
|
||||
38
packages/testing/playwright.config.ts
Normal file
38
packages/testing/playwright.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'setup',
|
||||
testMatch: /.*\.setup\.ts/,
|
||||
use: { ...devices['Desktop Chrome'] }
|
||||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: 'playwright/.auth/user.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
34
packages/testing/tsconfig.json
Normal file
34
packages/testing/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom", "node", "@playwright/test"],
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../apps/mail/*"],
|
||||
"@zero/server/*": ["../../apps/server/src/*"],
|
||||
"@zero/mail/*": ["../../apps/mail/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"e2e/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
1908
pnpm-lock.yaml
generated
1908
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user