Skip to content

Commit 84619a2

Browse files
committed
Add comprehensive frontend test suite
- Add Jest and React Testing Library for unit/component tests - Add Playwright for E2E tests - Create colocated test files for components: - Utility tests: Timeout, Defer, RoutingListener - Core components: AsyncButton, Console, AlternatingSections, MetaTags, Layout - Form components: NewsletterSection, ContactSection - Modal components: RouteHashBasedModal, Download - Add E2E tests for user flows: - Homepage, navigation, newsletter signup - Contact form, download modal, comparison page - Add mock files for Next.js router, Link, Head, Script - Add mocks for aphrodite, unfetch - Update README with testing instructions - Update CI workflow to run unit and E2E tests before build
1 parent 7ad0a10 commit 84619a2

32 files changed

+27536
-16052
lines changed

.github/workflows/frontend.yml

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,59 @@ concurrency:
1414
cancel-in-progress: false
1515

1616
jobs:
17+
test:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
- name: Use Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: 20.x
26+
cache: npm
27+
cache-dependency-path: frontend/package-lock.json
28+
- name: Install Dependencies
29+
working-directory: frontend
30+
run: npm install
31+
- name: Run Unit Tests
32+
working-directory: frontend
33+
run: npm test -- --ci --coverage
34+
- name: Upload Coverage Report
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: coverage-report
38+
path: frontend/coverage/
39+
40+
e2e:
41+
runs-on: ubuntu-latest
42+
steps:
43+
- name: Checkout
44+
uses: actions/checkout@v4
45+
- name: Use Node.js
46+
uses: actions/setup-node@v4
47+
with:
48+
node-version: 20.x
49+
cache: npm
50+
cache-dependency-path: frontend/package-lock.json
51+
- name: Install Dependencies
52+
working-directory: frontend
53+
run: npm install
54+
- name: Install Playwright Browsers
55+
working-directory: frontend
56+
run: npx playwright install --with-deps
57+
- name: Run E2E Tests
58+
working-directory: frontend
59+
run: npm run test:e2e
60+
- name: Upload Playwright Report
61+
uses: actions/upload-artifact@v4
62+
if: always()
63+
with:
64+
name: playwright-report
65+
path: frontend/playwright-report/
66+
retention-days: 30
67+
1768
build:
69+
needs: [test, e2e]
1870
runs-on: ubuntu-latest
1971
steps:
2072
- name: Checkout
@@ -24,7 +76,7 @@ jobs:
2476
with:
2577
node-version: 20.x
2678
cache: npm
27-
cache-dependency-path: frontend/package.json
79+
cache-dependency-path: frontend/package-lock.json
2880
- name: Setup Pages
2981
uses: actions/configure-pages@v5
3082
with:

frontend/README.md

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,132 @@ Build prod version: $ npm run build
99
Start built prod version: $ npm run start
1010
Export to static files: $ npm run export
1111

12+
Testing
13+
-------------
14+
15+
### Unit Tests
16+
17+
The project uses Jest and React Testing Library for unit and component tests. Test files are colocated with their source files using the `.test.tsx` or `.test.ts` extension.
18+
19+
**Run all unit tests:**
20+
```bash
21+
npm test
22+
```
23+
24+
**Run tests in watch mode (useful during development):**
25+
```bash
26+
npm run test:watch
27+
```
28+
29+
**Run tests with coverage report:**
30+
```bash
31+
npm run test:coverage
32+
```
33+
34+
The coverage report will be generated in the `coverage/` directory.
35+
36+
### End-to-End Tests
37+
38+
The project uses Playwright for end-to-end testing. E2E tests are located in the `e2e/` directory.
39+
40+
**Run E2E tests:**
41+
```bash
42+
npm run test:e2e
43+
```
44+
45+
**Run E2E tests in headed mode (see the browser):**
46+
```bash
47+
npm run test:e2e:headed
48+
```
49+
50+
**Run E2E tests for a specific browser:**
51+
```bash
52+
npx playwright test --project=chromium
53+
npx playwright test --project=firefox
54+
npx playwright test --project=webkit
55+
```
56+
57+
**View E2E test report:**
58+
```bash
59+
npx playwright show-report
60+
```
61+
62+
**Install Playwright browsers (first time setup):**
63+
```bash
64+
npx playwright install
65+
```
66+
67+
### Writing Tests
68+
69+
**Unit Tests:**
70+
- Place test files next to the source file: `Component.tsx``Component.test.tsx`
71+
- Use React Testing Library for component testing
72+
- Follow the existing patterns in the codebase
73+
- Mock external dependencies (fetch, analytics, Next.js router)
74+
75+
Example:
76+
```tsx
77+
import { render, screen } from '@testing-library/react';
78+
import userEvent from '@testing-library/user-event';
79+
import MyComponent from './MyComponent';
80+
81+
describe('MyComponent', () => {
82+
it('renders correctly', () => {
83+
render(<MyComponent />);
84+
expect(screen.getByText('Expected Text')).toBeInTheDocument();
85+
});
86+
87+
it('handles user interaction', async () => {
88+
const user = userEvent.setup();
89+
render(<MyComponent />);
90+
91+
await user.click(screen.getByRole('button'));
92+
93+
expect(screen.getByText('Clicked!')).toBeInTheDocument();
94+
});
95+
});
96+
```
97+
98+
**E2E Tests:**
99+
- Place test files in the `e2e/` directory with `.spec.ts` extension
100+
- Use Playwright's testing API
101+
- Mock API responses when testing form submissions
102+
103+
Example:
104+
```typescript
105+
import { test, expect } from '@playwright/test';
106+
107+
test('should complete user flow', async ({ page }) => {
108+
await page.goto('/');
109+
110+
await page.getByRole('button', { name: /submit/i }).click();
111+
112+
await expect(page.getByText('Success!')).toBeVisible();
113+
});
114+
```
115+
116+
### Test Structure
117+
118+
```
119+
frontend/
120+
├── components/
121+
│ ├── MyComponent.tsx
122+
│ ├── MyComponent.test.tsx # Unit test (colocated)
123+
│ └── ...
124+
├── e2e/
125+
│ ├── homepage.spec.ts # E2E tests
126+
│ ├── navigation.spec.ts
127+
│ └── ...
128+
├── __mocks__/ # Jest mocks
129+
│ ├── next/
130+
│ ├── aphrodite.js
131+
│ └── unfetch.js
132+
├── jest.config.js
133+
├── jest.setup.js
134+
└── playwright.config.ts
135+
```
136+
12137
Deployment
13138
-------------
14139
Circle CI, on a successful master build, exports the project to static files and
15140
pushes them to the 'gh-pages' branch, where they are picked up by GitHub Pages.
16-

frontend/__mocks__/aphrodite.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const StyleSheet = {
2+
create: (styles) => styles,
3+
rehydrate: jest.fn(),
4+
};
5+
6+
const css = (...args) =>
7+
args
8+
.filter(Boolean)
9+
.map((arg) => (typeof arg === 'object' ? Object.keys(arg).join(' ') : ''))
10+
.join(' ');
11+
12+
module.exports = {
13+
StyleSheet,
14+
css,
15+
};

frontend/__mocks__/fileMock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'test-file-stub';

frontend/__mocks__/next/head.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const React = require('react');
2+
3+
const Head = ({ children }) => {
4+
return React.createElement(React.Fragment, null, children);
5+
};
6+
7+
module.exports = Head;
8+
module.exports.default = Head;

frontend/__mocks__/next/link.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const React = require('react');
2+
3+
const Link = ({ children, href, legacyBehavior, passHref, prefetch, replace, scroll, shallow, locale, ...props }) => {
4+
// Filter out Next.js-specific props that shouldn't be passed to DOM elements
5+
return React.createElement(
6+
'a',
7+
{ href, ...props },
8+
children
9+
);
10+
};
11+
12+
module.exports = Link;
13+
module.exports.default = Link;

frontend/__mocks__/next/router.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const React = require('react');
2+
3+
const mockRouter = {
4+
asPath: '/',
5+
pathname: '/',
6+
query: {},
7+
push: jest.fn(() => Promise.resolve(true)),
8+
replace: jest.fn(() => Promise.resolve(true)),
9+
prefetch: jest.fn(() => Promise.resolve()),
10+
back: jest.fn(),
11+
forward: jest.fn(),
12+
reload: jest.fn(),
13+
events: {
14+
on: jest.fn(),
15+
off: jest.fn(),
16+
emit: jest.fn(),
17+
},
18+
isFallback: false,
19+
isReady: true,
20+
isPreview: false,
21+
};
22+
23+
const useRouter = jest.fn(() => mockRouter);
24+
25+
const withRouter = (Component) => {
26+
const WithRouterWrapper = (props) => {
27+
return React.createElement(Component, { ...props, router: mockRouter });
28+
};
29+
WithRouterWrapper.displayName = `withRouter(${Component.displayName || Component.name || 'Component'})`;
30+
return WithRouterWrapper;
31+
};
32+
33+
const Router = {
34+
...mockRouter,
35+
events: {
36+
on: jest.fn(),
37+
off: jest.fn(),
38+
emit: jest.fn(),
39+
},
40+
};
41+
42+
module.exports = {
43+
useRouter,
44+
withRouter,
45+
Router,
46+
default: Router,
47+
mockRouter,
48+
};

frontend/__mocks__/next/script.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const React = require('react');
2+
3+
const Script = ({ children, ...props }) => {
4+
return React.createElement('script', props, children);
5+
};
6+
7+
module.exports = Script;
8+
module.exports.default = Script;

frontend/__mocks__/unfetch.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const mockFetch = jest.fn(() =>
2+
Promise.resolve({
3+
ok: true,
4+
json: () => Promise.resolve({}),
5+
text: () => Promise.resolve(''),
6+
})
7+
);
8+
9+
module.exports = mockFetch;
10+
module.exports.default = mockFetch;

0 commit comments

Comments
 (0)