Skip to content

Commit 9ff16e7

Browse files
authored
Merge pull request #61 from raminr77/dev
feat: Add unit and e2e tests
2 parents d7a5e06 + faf4d89 commit 9ff16e7

File tree

16 files changed

+636
-393
lines changed

16 files changed

+636
-393
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
/coverage
1515
.jest-cache
1616

17+
# playwright
18+
/playwright-report
19+
/test-results
20+
.last-run.json
21+
1722
# next.js
1823
/.next/
1924
/out/

e2e/contact.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.emulateMedia({ reducedMotion: 'reduce' });
5+
});
6+
7+
test('contact form submits (mocked recaptcha + email)', async ({ page }) => {
8+
await page.route('**/recaptcha/api.js*', async (route) => {
9+
await route.fulfill({
10+
status: 200,
11+
contentType: 'application/javascript',
12+
body: [
13+
'window.grecaptcha = {',
14+
' ready: (cb) => cb && cb(),',
15+
' execute: () => Promise.resolve("e2e-token")',
16+
'};'
17+
].join('\n')
18+
});
19+
});
20+
21+
await page.route('**/api/recaptcha-verify', async (route) => {
22+
await route.fulfill({
23+
status: 200,
24+
contentType: 'application/json',
25+
body: JSON.stringify({ success: true, message: 'e2e ok' })
26+
});
27+
});
28+
29+
await page.route('https://email-api.ramiin.workers.dev/**', async (route) => {
30+
await route.fulfill({
31+
status: 200,
32+
contentType: 'application/json',
33+
body: JSON.stringify({ success: true, message: 'E2E: message sent' })
34+
});
35+
});
36+
37+
await page.goto('/contact-me/');
38+
39+
await page.getByTestId('subject-input').fill('Hello from Playwright');
40+
await page.getByTestId('email-input').fill('e2e@example.com');
41+
await page
42+
.getByTestId('message-input')
43+
.fill('This is an end-to-end test message that is long enough.');
44+
45+
await page.getByTestId('submit-button').click();
46+
await expect(page.getByText('E2E: message sent')).toBeVisible();
47+
});

e2e/navigation.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.emulateMedia({ reducedMotion: 'reduce' });
5+
});
6+
7+
test('main navigation works', async ({ page }) => {
8+
await page.goto('/');
9+
10+
await expect(
11+
page.getByRole('link', {
12+
name: /download\s+software-engineer-ramin-rezaei-cv-2025\.pdf/i
13+
})
14+
).toBeVisible();
15+
16+
await page.getByRole('link', { name: 'Posts' }).click();
17+
await expect(page).toHaveURL(/\/posts\/?$/);
18+
await expect(page.getByRole('heading', { name: /post/i })).toBeVisible();
19+
20+
await page.getByRole('link', { name: 'Home' }).click();
21+
await expect(page).toHaveURL(/\/$/);
22+
});

e2e/posts.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.emulateMedia({ reducedMotion: 'reduce' });
5+
});
6+
7+
test('can open a post from the list and return back', async ({ page }) => {
8+
await page.goto('/posts/');
9+
10+
const firstReadMore = page.getByRole('link', { name: '[ Read More ]' }).first();
11+
await expect(firstReadMore).toBeVisible();
12+
await firstReadMore.click();
13+
14+
await expect(page).toHaveURL(/\/posts\/\d+/);
15+
await expect(page.locator('main h3').first()).toBeVisible();
16+
17+
const backLink = page.getByRole('link', { name: 'Back To All Posts' });
18+
await expect(backLink).toBeVisible();
19+
await backLink.click();
20+
await expect(page).toHaveURL(/\/posts\/?$/);
21+
});

e2e/routing.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.emulateMedia({ reducedMotion: 'reduce' });
5+
});
6+
7+
test('legacy routes redirect', async ({ page }) => {
8+
await page.goto('/skills');
9+
await expect(page).toHaveURL(/\/journey\/?$/);
10+
});
11+
12+
test('unknown routes show not-found page', async ({ page }) => {
13+
await page.goto('/definitely-not-a-real-route');
14+
await expect(page.getByRole('link', { name: 'Return Home' })).toBeVisible();
15+
});
16+
17+
test('invalid post id redirects to posts list', async ({ page }) => {
18+
await page.goto('/posts/999999');
19+
await expect(page).toHaveURL(/\/posts\/?$/);
20+
});

e2e/search.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.emulateMedia({ reducedMotion: 'reduce' });
5+
});
6+
7+
test('post search modal returns results and can navigate', async ({ page }) => {
8+
await page.goto('/posts/');
9+
10+
await page.getByRole('button', { name: 'Search Posts' }).click();
11+
12+
const searchInput = page.getByTestId('search');
13+
await expect(searchInput).toBeVisible();
14+
await searchInput.fill('property');
15+
16+
const modal = page.getByRole('button', { name: 'Close Search' }).locator('..');
17+
const firstResult = modal.locator('ul a').first();
18+
await expect(firstResult).toBeVisible();
19+
20+
await firstResult.click();
21+
await expect(page).toHaveURL(/\/posts\/\d+/);
22+
await expect(page.locator('main h3').first()).toBeVisible();
23+
});

e2e/theme.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.emulateMedia({ reducedMotion: 'reduce' });
5+
});
6+
7+
test('theme toggle switches html class and persists after reload', async ({ page }) => {
8+
await page.goto('/');
9+
10+
const html = page.locator('html');
11+
await expect(html).toHaveClass(/\bdark\b/);
12+
13+
await page.getByRole('button', { name: 'Toggle theme' }).click();
14+
await expect(html).toHaveClass(/\blight\b/);
15+
16+
await page.reload();
17+
await expect(html).toHaveClass(/\blight\b/);
18+
});

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export default defineConfig(
99
'.git/',
1010
'.husky/',
1111
'.next/',
12+
'coverage/',
13+
'playwright-report/',
14+
'test-results/',
1215
'public/',
1316
'.github/',
1417
'.github/**/*.yml',

jest.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ const customJestConfig = {
1313
testEnvironment: 'jest-environment-jsdom',
1414
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
1515
moduleDirectories: ['node_modules', '<rootDir>/src'],
16-
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/']
16+
testPathIgnorePatterns: [
17+
'<rootDir>/.next/',
18+
'<rootDir>/node_modules/',
19+
'<rootDir>/e2e/'
20+
]
1721
};
1822

1923
module.exports = createJestConfig(customJestConfig);

package.json

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "raminrezaei",
3-
"version": "5.2.4",
3+
"version": "5.3.0",
44
"private": true,
55
"scripts": {
66
"lighthouse": "lighthouse http://localhost:3000 --output html --output-path ./lighthouse-report.html --chrome-flags=\"--headless\"",
@@ -19,13 +19,17 @@
1919
"prepare": "husky",
2020
"test": "jest",
2121
"test:watch": "jest --watch",
22-
"test:cov": "jest --coverage"
22+
"test:coverage": "jest --coverage",
23+
"test:e2e": "playwright test",
24+
"test:e2e:ui": "playwright test --ui",
25+
"test:e2e:report": "playwright show-report",
26+
"test:e2e:install": "playwright install"
2327
},
2428
"dependencies": {
2529
"@gsap/react": "^2.1.2",
2630
"@next/bundle-analyzer": "^16.1.6",
2731
"@next/third-parties": "^16.1.6",
28-
"@sentry/nextjs": "^10.39.0",
32+
"@sentry/nextjs": "^10.41.0",
2933
"@tailwindcss/postcss": "^4.2.1",
3034
"@vercel/speed-insights": "^1.3.1",
3135
"animate.css": "^4.1.1",
@@ -34,7 +38,7 @@
3438
"gray-matter": "^4.0.3",
3539
"gsap": "^3.14.2",
3640
"markdown-to-jsx": "^9.7.6",
37-
"motion": "^12.34.3",
41+
"motion": "^12.34.4",
3842
"next": "^16.1.6",
3943
"qs": "^6.15.0",
4044
"react": "^19.2.4",
@@ -53,13 +57,14 @@
5357
"@eslint/js": "^10.0.1",
5458
"@microsoft/eslint-formatter-sarif": "^3.1.0",
5559
"@next/eslint-plugin-next": "^16.1.6",
60+
"@playwright/test": "^1.58.2",
5661
"@testing-library/dom": "^10.4.1",
5762
"@testing-library/jest-dom": "^6.9.1",
5863
"@testing-library/react": "^16.3.2",
5964
"@testing-library/user-event": "^14.6.1",
6065
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
6166
"@types/jest": "^30.0.0",
62-
"@types/node": "^25.3.0",
67+
"@types/node": "^25.3.3",
6368
"@types/qs": "^6.14.0",
6469
"@types/react": "^19.2.14",
6570
"@types/react-dom": "^19.2.3",
@@ -70,8 +75,8 @@
7075
"jest": "^30.2.0",
7176
"jest-environment-jsdom": "^30.2.0",
7277
"lighthouse": "^13.0.3",
73-
"lint-staged": "^16.2.7",
74-
"postcss": "^8.5.6",
78+
"lint-staged": "^16.3.1",
79+
"postcss": "^8.5.8",
7580
"prettier": "3.8.1",
7681
"prettier-plugin-sort-imports": "^1.8.11",
7782
"prettier-plugin-tailwindcss": "^0.7.2",
@@ -82,7 +87,7 @@
8287
"ts-node": "^10.9.2",
8388
"typescript": "^5.9.3",
8489
"typescript-eslint": "^8.56.1",
85-
"webpack": "^5.105.2",
90+
"webpack": "^5.105.3",
8691
"webpack-bundle-analyzer": "^5.2.0"
8792
},
8893
"browserslist": {
@@ -99,5 +104,5 @@
99104
"last 1 safari version"
100105
]
101106
},
102-
"packageManager": "pnpm@10.30.2"
107+
"packageManager": "pnpm@10.30.3"
103108
}

0 commit comments

Comments
 (0)