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:
amrit
2025-08-01 20:59:43 +05:30
committed by GitHub
parent 908fdbb7d9
commit e1cdeb82c2
8 changed files with 2187 additions and 5 deletions

View File

@@ -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 =

View File

@@ -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",

View 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!');
});

View 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!');
});
});

View 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"
}
}

View 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'],
},
],
});

View 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

File diff suppressed because it is too large Load Diff