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:
shancheas
2026-08-31 11:52:27 +07:00
parent 166e0d40ac
commit 67e1e0a74f
23 changed files with 318 additions and 108 deletions
+4
View File
@@ -0,0 +1,4 @@
export const E2E_LOGIN = {
username: 'administrator',
password: 'password123',
} as const;
+11
View File
@@ -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`;
}
+91
View File
@@ -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();
}