feat: integrate Playwright for E2E testing and enhance testing framework
- Added Playwright for end-to-end testing in the `apps/web` module, including a new `login.spec.ts` for testing login functionality. - Updated package.json to include Playwright dependencies and new test commands for E2E testing. - Enhanced .gitignore to exclude Playwright test results and reports. - Modified existing documentation to reflect the integration of Playwright and updated testing guidelines. - Refactored test commands to streamline the testing process, including a dedicated command for E2E tests. These changes improve the testing framework by providing robust E2E testing capabilities, enhancing the reliability and quality of the application.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export const E2E_LOGIN = {
|
||||
username: 'administrator',
|
||||
password: 'password123',
|
||||
} as const;
|
||||
@@ -0,0 +1,11 @@
|
||||
export function createFakeJwt(expiresInSeconds = 3600): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url');
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({
|
||||
sub: 'e2e-user',
|
||||
exp: Math.floor(Date.now() / 1000) + expiresInSeconds,
|
||||
}),
|
||||
).toString('base64url');
|
||||
|
||||
return `${header}.${payload}.e2e`;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Page, Route } from '@playwright/test';
|
||||
import { E2E_LOGIN } from './credentials';
|
||||
import { createFakeJwt } from './fake-jwt';
|
||||
|
||||
const APP_HOSTS = new Set(['127.0.0.1', 'localhost']);
|
||||
const APP_PORT = Number(process.env.E2E_PORT ?? 4173);
|
||||
|
||||
const EMPTY_LIST = {
|
||||
data: [],
|
||||
meta: {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
itemCount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const E2E_USER = {
|
||||
id: 'e2e-user-id',
|
||||
username: E2E_LOGIN.username,
|
||||
isSuperadmin: true,
|
||||
privilege: null,
|
||||
permissions: {},
|
||||
};
|
||||
|
||||
export type LoginApiOutcome = 'success' | 'unauthorized';
|
||||
|
||||
export async function mockBackend(page: Page, login: LoginApiOutcome = 'success'): Promise<void> {
|
||||
await page.route('**/*', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (isAppRequest(requestUrl)) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillBackend(route, login);
|
||||
});
|
||||
}
|
||||
|
||||
function isAppRequest(requestUrl: URL): boolean {
|
||||
const port = Number(requestUrl.port || (requestUrl.protocol === 'https:' ? 443 : 80));
|
||||
return APP_HOSTS.has(requestUrl.hostname) && port === APP_PORT;
|
||||
}
|
||||
|
||||
async function fulfillBackend(route: Route, login: LoginApiOutcome): Promise<void> {
|
||||
const request = route.request();
|
||||
const pathname = new URL(request.url()).pathname.replace(/\/$/, '');
|
||||
|
||||
if (pathname.endsWith('/auth/login') && request.method() === 'POST') {
|
||||
if (login === 'unauthorized') {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ statusCode: 401, message: 'Unauthorized' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
accessToken: createFakeJwt(),
|
||||
refreshToken: createFakeJwt(86_400),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith('/auth/me') && request.method() === 'GET') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(E2E_USER),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceType = request.resourceType();
|
||||
if (resourceType === 'xhr' || resourceType === 'fetch') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(EMPTY_LIST),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.abort();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { E2E_LOGIN } from './fixtures/credentials';
|
||||
import { mockBackend } from './fixtures/mock-api';
|
||||
|
||||
test.describe('login', () => {
|
||||
test('renders the login form', async ({ page }) => {
|
||||
await mockBackend(page);
|
||||
await page.goto('/auth/login');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Login to your account!' })).toBeVisible();
|
||||
await expect(page.getByLabel('Username')).toBeVisible();
|
||||
await expect(page.getByLabel('Password')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('signs in with valid credentials and lands in the app', async ({ page }) => {
|
||||
await mockBackend(page, 'success');
|
||||
await page.goto('/auth/login');
|
||||
|
||||
await page.getByLabel('Username').fill(E2E_LOGIN.username);
|
||||
await page.getByLabel('Password').fill(E2E_LOGIN.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/app(\/|$)/);
|
||||
await expect(page.getByRole('heading', { name: 'Full Page' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows an error and stays on login when credentials are rejected', async ({ page }) => {
|
||||
await mockBackend(page, 'unauthorized');
|
||||
await page.goto('/auth/login');
|
||||
|
||||
await page.getByLabel('Username').fill(E2E_LOGIN.username);
|
||||
await page.getByLabel('Password').fill(E2E_LOGIN.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByText('Invalid username or password')).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/auth\/login/);
|
||||
await expect(page.getByRole('heading', { name: 'Login to your account!' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -8,9 +8,12 @@
|
||||
"dev": "pnpm run copy:brand && vite --clearScreen false",
|
||||
"build": "pnpm run copy:brand && tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"lint": "eslint \"src/**/*.ts\" \"e2e/**/*.ts\" \"playwright.config.ts\"",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest --watch",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:install": "playwright install chromium",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -37,6 +40,7 @@
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/react": "^19.2.7",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const E2E_PORT = Number(process.env.E2E_PORT ?? 4173);
|
||||
const E2E_ORIGIN = `http://127.0.0.1:${E2E_PORT}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: 'list',
|
||||
timeout: 60_000,
|
||||
use: {
|
||||
baseURL: E2E_ORIGIN,
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
webServer: {
|
||||
command: `pnpm run copy:brand && vite --clearScreen false --host 127.0.0.1 --port ${E2E_PORT} --strictPort`,
|
||||
url: E2E_ORIGIN,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
});
|
||||
@@ -21,5 +21,6 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: false,
|
||||
exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**'],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user