Skip to content

Commit e1cdeb8

Browse files
authored
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 -->
1 parent 908fdbb commit e1cdeb8

File tree

8 files changed

+2187
-5
lines changed

8 files changed

+2187
-5
lines changed

.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ AUTUMN_SECRET_KEY=
3737

3838
TWILIO_ACCOUNT_SID=
3939
TWILIO_AUTH_TOKEN=
40-
TWILIO_PHONE_NUMBER=
40+
TWILIO_PHONE_NUMBER=
41+
42+
# FOR PLAYWRIGHT E2E TESTING
43+
PLAYWRIGHT_SESSION_TOKEN =
44+
PLAYWRIGHT_SESSION_DATA =
45+
EMAIL =

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
"db:studio": "dotenv -- pnpm run -C apps/server db:studio",
3030
"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",
3131
"scripts": "dotenv -- pnpx tsx ./scripts/run.ts",
32+
"test": "pnpm --filter=@zero/testing test",
33+
"test:watch": "pnpm --filter=@zero/testing test:watch",
34+
"test:coverage": "pnpm --filter=@zero/testing test:coverage",
35+
"test:ui": "pnpm --filter=@zero/testing test:ui",
3236
"test:ai": "dotenv -- pnpm --filter=@zero/server run test:ai",
3337
"eval": "dotenv -- pnpm --filter=@zero/server run eval",
3438
"eval:dev": "dotenv -- pnpm --filter=@zero/server run eval:dev",

packages/testing/e2e/auth.setup.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { test as setup } from '@playwright/test';
2+
import path from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
7+
8+
setup('inject real authentication session', async ({ page }) => {
9+
console.log('Injecting real authentication session...');
10+
11+
const SessionToken = process.env.PLAYWRIGHT_SESSION_TOKEN;
12+
const SessionData = process.env.PLAYWRIGHT_SESSION_DATA;
13+
14+
if (!SessionToken || !SessionData) {
15+
throw new Error('PLAYWRIGHT_SESSION_TOKEN and PLAYWRIGHT_SESSION_DATA environment variables must be set.');
16+
}
17+
18+
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
19+
20+
console.log('Page loaded, setting up authentication...');
21+
22+
// sets better auth session cookies
23+
await page.context().addCookies([
24+
{
25+
name: 'better-auth-dev.session_token',
26+
value: SessionToken,
27+
domain: 'localhost',
28+
path: '/',
29+
httpOnly: true,
30+
secure: false,
31+
sameSite: 'Lax'
32+
},
33+
{
34+
name: 'better-auth-dev.session_data',
35+
value: SessionData,
36+
domain: 'localhost',
37+
path: '/',
38+
httpOnly: true,
39+
secure: false,
40+
sameSite: 'Lax'
41+
}
42+
]);
43+
44+
console.log('Real session cookies injected');
45+
46+
try {
47+
const decodedSessionData = JSON.parse(atob(SessionData));
48+
49+
await page.addInitScript((sessionData) => {
50+
if (sessionData.session) {
51+
localStorage.setItem('better-auth.session', JSON.stringify(sessionData.session.session));
52+
localStorage.setItem('better-auth.user', JSON.stringify(sessionData.session.user));
53+
}
54+
}, decodedSessionData);
55+
56+
console.log('Session data set in localStorage');
57+
} catch (error) {
58+
console.log('Could not decode session data for localStorage:', error);
59+
}
60+
61+
await page.goto('/mail/inbox');
62+
await page.waitForLoadState('domcontentloaded');
63+
64+
const currentUrl = page.url();
65+
console.log('Current URL after clicking Get Started:', currentUrl);
66+
67+
if (currentUrl.includes('/mail')) {
68+
console.log('Successfully reached mail app! On:', currentUrl);
69+
} else {
70+
console.log('Did not reach mail app. Current URL:', currentUrl);
71+
await page.screenshot({ path: 'debug-auth-failed.png' });
72+
}
73+
74+
await page.context().storageState({ path: authFile });
75+
76+
console.log('Real authentication session injected and saved!');
77+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const email = process.env.EMAIL;
4+
5+
if (!email) {
6+
throw new Error('EMAIL environment variable must be set.');
7+
}
8+
9+
test.describe('Signing In, Sending mail, Replying to a mail', () => {
10+
test('should send and reply to an email in the same session', async ({ page }) => {
11+
await page.goto('/mail/inbox');
12+
await page.waitForLoadState('domcontentloaded');
13+
console.log('Successfully accessed mail inbox');
14+
15+
await page.waitForTimeout(2000);
16+
try {
17+
const welcomeModal = page.getByText('Welcome to Zero Email!');
18+
if (await welcomeModal.isVisible({ timeout: 2000 })) {
19+
console.log('Onboarding modal detected, clicking outside to dismiss...');
20+
await page.locator('body').click({ position: { x: 100, y: 100 } });
21+
await page.waitForTimeout(1500);
22+
console.log('Modal successfully dismissed');
23+
}
24+
} catch {
25+
console.log('No onboarding modal found, proceeding...');
26+
}
27+
28+
await expect(page.getByText('Inbox')).toBeVisible();
29+
console.log('Mail inbox is now visible');
30+
31+
console.log('Starting email sending process...');
32+
await page.getByText('New email').click();
33+
await page.waitForTimeout(2000);
34+
35+
await page.locator('input').first().fill(email);
36+
console.log('Filled To: field');
37+
38+
await page.getByRole('button', { name: 'Send' }).click();
39+
console.log('Clicked Send button');
40+
await page.waitForTimeout(3000);
41+
console.log('Email sent successfully!');
42+
43+
console.log('Waiting for email to arrive...');
44+
await page.waitForTimeout(10000);
45+
46+
console.log('Looking for the first email in the list...');
47+
await page.locator('[data-thread-id]').first().click();
48+
console.log('Clicked on email (PM/AM area).');
49+
50+
console.log('Looking for Reply button to confirm email is open...');
51+
await page.waitForTimeout(2000);
52+
53+
const replySelectors = [
54+
'button:has-text("Reply")',
55+
'[data-testid*="reply"]',
56+
'button[title*="Reply"]',
57+
'button:text-is("Reply")',
58+
'button:text("Reply")'
59+
];
60+
61+
let replyClicked = false;
62+
for (const selector of replySelectors) {
63+
try {
64+
await page.locator(selector).first().click({ force: true });
65+
console.log(`Clicked Reply button using: ${selector}`);
66+
replyClicked = true;
67+
break;
68+
} catch {
69+
console.log(`Failed to click with ${selector}`);
70+
}
71+
}
72+
73+
if (!replyClicked) {
74+
console.log('Could not find Reply button');
75+
}
76+
77+
await page.waitForTimeout(2000);
78+
79+
console.log('Sending reply...');
80+
await page.getByRole('button', { name: 'Send' }).click();
81+
await page.waitForTimeout(3000);
82+
console.log('Reply sent successfully!');
83+
84+
console.log('Entire email flow completed successfully!');
85+
});
86+
});

packages/testing/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@zero/testing",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"test:e2e": "playwright test",
8+
"test:e2e:ui": "playwright test --ui",
9+
"test:e2e:debug": "playwright test --debug",
10+
"test:e2e:headed": "playwright test --headed"
11+
},
12+
"devDependencies": {
13+
"@cloudflare/playwright": "0.0.11",
14+
"@playwright/test": "^1.40.0",
15+
"@testing-library/jest-dom": "^6.1.4",
16+
"@testing-library/react": "^14.1.2",
17+
"@testing-library/user-event": "^14.5.1",
18+
"@types/testing-library__jest-dom": "^6.0.0",
19+
"@vitejs/plugin-react": "^4.7.0",
20+
"@vitest/coverage-v8": "^1.0.4",
21+
"@vitest/ui": "^1.0.4",
22+
"happy-dom": "^12.10.3",
23+
"jsdom": "^23.0.1",
24+
"msw": "^2.0.8",
25+
"dotenv": "^16.3.1",
26+
"typescript": "^5.4.0",
27+
"vitest": "^1.0.4"
28+
},
29+
"peerDependencies": {
30+
"@types/react": "*",
31+
"@types/react-dom": "*",
32+
"react": "*",
33+
"react-dom": "*"
34+
},
35+
"dependencies": {
36+
"@tanstack/react-query": "^5.81.5"
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
import dotenv from 'dotenv';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
10+
11+
export default defineConfig({
12+
testDir: './e2e',
13+
fullyParallel: false,
14+
forbidOnly: !!process.env.CI,
15+
retries: process.env.CI ? 2 : 0,
16+
workers: 1,
17+
reporter: 'html',
18+
use: {
19+
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
20+
trace: 'on-first-retry',
21+
screenshot: 'only-on-failure',
22+
},
23+
projects: [
24+
{
25+
name: 'setup',
26+
testMatch: /.*\.setup\.ts/,
27+
use: { ...devices['Desktop Chrome'] }
28+
},
29+
{
30+
name: 'chromium',
31+
use: {
32+
...devices['Desktop Chrome'],
33+
storageState: 'playwright/.auth/user.json',
34+
},
35+
dependencies: ['setup'],
36+
},
37+
],
38+
});

packages/testing/tsconfig.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
5+
"module": "ESNext",
6+
"skipLibCheck": true,
7+
"types": ["vitest/globals", "@testing-library/jest-dom", "node", "@playwright/test"],
8+
"moduleResolution": "bundler",
9+
"allowImportingTsExtensions": true,
10+
"resolveJsonModule": true,
11+
"isolatedModules": true,
12+
"noEmit": true,
13+
"jsx": "react-jsx",
14+
"jsxImportSource": "react",
15+
"strict": true,
16+
"noUnusedLocals": false,
17+
"noUnusedParameters": false,
18+
"noFallthroughCasesInSwitch": true,
19+
"baseUrl": ".",
20+
"paths": {
21+
"@/*": ["../../apps/mail/*"],
22+
"@zero/server/*": ["../../apps/server/src/*"],
23+
"@zero/mail/*": ["../../apps/mail/*"]
24+
}
25+
},
26+
"include": [
27+
"**/*.ts",
28+
"**/*.tsx",
29+
"**/*.test.ts",
30+
"**/*.test.tsx",
31+
"e2e/**/*.ts"
32+
],
33+
"exclude": ["node_modules", "dist"]
34+
}

0 commit comments

Comments
 (0)