Skip to content

Commit 6b71b98

Browse files
committed
Add Playwright for end-to-end testing and configure test setup
- Added Playwright as a development dependency for end-to-end testing. - Created a Playwright configuration file to define test settings and browser projects. - Implemented basic end-to-end tests for the NotificationAPI React SDK. - Updated GitHub Actions workflow to install Playwright browsers and run E2E tests. - Enhanced .gitignore to exclude Playwright test results and reports.
1 parent 626b38c commit 6b71b98

File tree

7 files changed

+286
-3
lines changed

7 files changed

+286
-3
lines changed

.github/workflows/pull_request.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ jobs:
2929

3030
- name: Build Project
3131
run: npm run build
32+
33+
- name: Install Playwright Browsers
34+
run: npx playwright install --with-deps
35+
36+
- name: Run E2E Tests
37+
run: npm run test:e2e
38+
39+
- name: Upload Playwright Report
40+
uses: actions/upload-artifact@v4
41+
if: failure()
42+
with:
43+
name: playwright-report
44+
path: playwright-report/
45+
retention-days: 30

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,10 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
26+
# Playwright
27+
/test-results/
28+
/playwright-report/
29+
/blob-report/
30+
/playwright/.cache/
31+
tests/e2e/screenshots/

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
"format": "prettier --write \"{src,lib}/**/*.{ts,tsx,js,jsx,json,css,md,html}\"",
2626
"prettier-check": "prettier --check \"{src,lib}/**/*.{ts,tsx,js,jsx,json,css,md,html}\"",
2727
"preview": "vite preview",
28-
"prepublishOnly": "npm run build"
28+
"prepublishOnly": "npm run build",
29+
"test:e2e": "playwright test",
30+
"test:e2e:ui": "playwright test --ui",
31+
"test:e2e:headed": "playwright test --headed"
2932
},
3033
"peerDependencies": {
3134
"react": "^17.0.2 || ^18.0.0 || ^19.0.0",
@@ -45,6 +48,7 @@
4548
"eslint-plugin-react-refresh": "^0.4.6",
4649
"faker-js": "^1.0.0",
4750
"glob": "^10.4.1",
51+
"@playwright/test": "^1.48.0",
4852
"prettier": "^3.3.3",
4953
"typescript": "^5.2.2",
5054
"vite": "^5.4.18",

playwright.config.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
/**
4+
* @see https://playwright.dev/docs/test-configuration
5+
*/
6+
export default defineConfig({
7+
testDir: './tests/e2e',
8+
/* Run tests in files in parallel */
9+
fullyParallel: true,
10+
/* Fail the build on CI if you accidentally left test.only in the source code. */
11+
forbidOnly: !!process.env.CI,
12+
/* Retry on CI only */
13+
retries: process.env.CI ? 2 : 0,
14+
/* Opt out of parallel tests on CI. */
15+
workers: process.env.CI ? 1 : undefined,
16+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
17+
reporter: 'html',
18+
/* Global timeout for each test - 5 seconds max */
19+
timeout: 5000,
20+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
21+
use: {
22+
/* Base URL to use in actions like `await page.goto('/')`. */
23+
baseURL: 'http://localhost:5173',
24+
25+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
26+
trace: 'on-first-retry',
27+
28+
/* Take screenshot on failure */
29+
screenshot: 'only-on-failure',
30+
31+
/* Action timeout - 5 seconds max for any single action */
32+
actionTimeout: 5000,
33+
34+
/* Navigation timeout - 5 seconds max for page loads */
35+
navigationTimeout: 5000
36+
},
37+
38+
/* Configure projects for major browsers */
39+
projects: [
40+
{
41+
name: 'chromium',
42+
use: { ...devices['Desktop Chrome'] }
43+
},
44+
45+
{
46+
name: 'firefox',
47+
use: { ...devices['Desktop Firefox'] }
48+
},
49+
50+
{
51+
name: 'webkit',
52+
use: { ...devices['Desktop Safari'] }
53+
},
54+
55+
/* Test against mobile viewports. */
56+
{
57+
name: 'Mobile Chrome',
58+
use: { ...devices['Pixel 5'] }
59+
},
60+
{
61+
name: 'Mobile Safari',
62+
use: { ...devices['iPhone 12'] }
63+
}
64+
65+
/* Test against branded browsers. */
66+
// {
67+
// name: 'Microsoft Edge',
68+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
69+
// },
70+
// {
71+
// name: 'Google Chrome',
72+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
73+
// },
74+
],
75+
76+
/* Run your local dev server before starting the tests */
77+
webServer: {
78+
command: 'npm run dev',
79+
url: 'http://localhost:5173',
80+
reuseExistingServer: !process.env.CI,
81+
timeout: 10000 // Allow 10 seconds for server startup
82+
}
83+
});

src/LiveComponents.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ import {
88
NotificationPreferencesPopup,
99
NotificationPreferencesInline
1010
} from '../lib/main';
11-
import { Button, Divider, TextField, Grid2, Alert } from '@mui/material';
11+
import {
12+
Button,
13+
Divider,
14+
TextField,
15+
Grid2,
16+
Alert,
17+
Switch,
18+
FormControlLabel
19+
} from '@mui/material';
1220

1321
interface LiveComponentsProps {
1422
isMocked: boolean;
@@ -49,6 +57,7 @@ const LiveComponents: React.FC<LiveComponentsProps> = ({
4957
const [clientId, setClientId] = useState('24nojpnrsdc53fkslha0roov05');
5058
const [userId, setUserId] = useState('sahand');
5159
const [apiUrl, setApiUrl] = useState('api.notificationapi.com');
60+
const [debugMode, setDebugMode] = useState(true);
5261
const [error] = useState<string | null>(null);
5362
const [preferencesPopupVisibility, setPreferencesPopupVisiblity] =
5463
useState(false);
@@ -88,6 +97,16 @@ const LiveComponents: React.FC<LiveComponentsProps> = ({
8897
variant="outlined"
8998
size="small"
9099
/>
100+
<FormControlLabel
101+
control={
102+
<Switch
103+
checked={debugMode}
104+
onChange={(e) => setDebugMode(e.target.checked)}
105+
color="primary"
106+
/>
107+
}
108+
label="🐛 Debug Mode"
109+
/>
91110
</Grid2>
92111

93112
<Grid2 container>
@@ -106,17 +125,23 @@ const LiveComponents: React.FC<LiveComponentsProps> = ({
106125
{error}
107126
</Alert>
108127
)}
128+
{debugMode && (
129+
<Alert severity="info" sx={{ margin: 2 }}>
130+
🐛 Debug mode is enabled! Check the browser console for detailed logs.
131+
</Alert>
132+
)}
109133
<div
110134
style={{
111135
padding: 24
112136
}}
113137
>
114-
<ErrorBoundary key={`${clientId}-${userId}-${apiUrl}`}>
138+
<ErrorBoundary key={`${clientId}-${userId}-${apiUrl}-${debugMode}`}>
115139
<NotificationAPIProvider
116140
clientId={clientId}
117141
userId={userId}
118142
apiURL={apiUrl}
119143
playSoundOnNewNotification={true}
144+
debug={debugMode}
120145
>
121146
<h2>Popup:</h2>
122147
<NotificationPopup />

tests/e2e/simple.spec.ts

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+
test.describe('NotificationAPI React SDK - Basic Tests', () => {
4+
test('app loads and renders root element', async ({ page }) => {
5+
await page.goto('/', { waitUntil: 'domcontentloaded' });
6+
7+
// Quick check that React app root exists
8+
const root = page.locator('#root');
9+
await expect(root).toBeVisible({ timeout: 3000 });
10+
});
11+
12+
test('app is responsive on mobile viewport', async ({ page }) => {
13+
await page.setViewportSize({ width: 375, height: 667 });
14+
await page.goto('/', { waitUntil: 'domcontentloaded' });
15+
16+
const root = page.locator('#root');
17+
await expect(root).toBeVisible({ timeout: 3000 });
18+
});
19+
20+
test('homepage loads successfully', async ({ page }) => {
21+
// Navigate to the development server
22+
await page.goto('/', { waitUntil: 'domcontentloaded' }); // Faster than waiting for all resources
23+
24+
// Quick check that page loaded
25+
await expect(page.locator('#root')).toBeVisible();
26+
});
27+
28+
test('basic navigation works', async ({ page }) => {
29+
await page.goto('/', { waitUntil: 'domcontentloaded' });
30+
31+
// Test that React app is rendered
32+
const root = page.locator('#root');
33+
await expect(root).toBeVisible();
34+
});
35+
36+
test('responsive design on mobile', async ({ page }) => {
37+
// Set mobile viewport
38+
await page.setViewportSize({ width: 375, height: 667 });
39+
await page.goto('/', { waitUntil: 'domcontentloaded' });
40+
41+
// Verify mobile-friendly elements
42+
const root = page.locator('#root');
43+
await expect(root).toBeVisible();
44+
});
45+
});
46+
47+
test.describe('NotificationAPI Components', () => {
48+
test('notification components render correctly', async ({ page }) => {
49+
await page.goto('/', { waitUntil: 'domcontentloaded' });
50+
51+
// Quick check for notification components
52+
// Using more lenient checks since we have strict timeouts
53+
54+
// Test notification trigger button (if exists)
55+
const notificationButton = page.locator(
56+
'[data-testid="notification-button"]'
57+
);
58+
const buttonExists = (await notificationButton.count()) > 0;
59+
60+
if (buttonExists) {
61+
await notificationButton.click();
62+
63+
// Quick verification without waiting too long
64+
const notificationModal = page.locator(
65+
'[data-testid="notification-modal"]'
66+
);
67+
await expect(notificationModal).toBeVisible({ timeout: 2000 });
68+
}
69+
});
70+
71+
test('notification list functionality', async ({ page }) => {
72+
await page.goto('/', { waitUntil: 'domcontentloaded' });
73+
74+
// Test notification list if it exists - quick check
75+
const notificationList = page.locator('[data-testid="notification-list"]');
76+
const listExists = (await notificationList.count()) > 0;
77+
78+
if (listExists) {
79+
// Quick verification of list items
80+
const listItems = page.locator('[data-testid="notification-item"]');
81+
if ((await listItems.count()) > 0) {
82+
await expect(listItems.first()).toBeVisible({ timeout: 2000 });
83+
}
84+
}
85+
});
86+
});

0 commit comments

Comments
 (0)