R: Add e2e tests for login

This commit is contained in:
Ivan 2025-03-06 21:09:44 +03:00
parent 31c32d5983
commit 7252290416
12 changed files with 120 additions and 33 deletions

View File

@ -42,6 +42,7 @@ jobs:
run: |
npm run lint
npm test
npm run teste2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:

21
.vscode/launch.json vendored
View File

@ -46,9 +46,28 @@
"args": ["test", "-k", "${fileBasenameNoExtension}"],
"django": true
},
{
// Run Tests for frontend for current file in Debug mode
"name": "FE-DebugTestFile",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/rsconcept/frontend",
"runtimeExecutable": "npx",
"runtimeArgs": [
"playwright",
"test",
"${fileBasename}",
"--headed",
"--project=Desktop Chrome"
],
"env": {
"PWDEBUG": "1"
},
"console": "integratedTerminal"
},
{
// Run Tests for frontned in Debug mode
"name": "FE-DebugTestAll",
"name": "Jest DebugAll",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest",

View File

@ -45,6 +45,9 @@ export default [
},
settings: { react: { version: 'detect' } },
rules: {
'no-console': 'off',
'require-jsdoc': 'off',
'react-compiler/react-compiler': 'error',
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
@ -112,16 +115,12 @@ export default [
rules: {
...playwright.configs['flat/recommended'].rules,
'no-console': 'off',
'require-jsdoc': 'off',
'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
'simple-import-sort/imports': 'warn'
}
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-console': 'off',
'require-jsdoc': 'off'
}
}
];

View File

@ -25,7 +25,7 @@ export default defineConfig({
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
port: 3000,
reuseExistingServer: !process.env.CI
}
});

View File

@ -36,13 +36,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
const router = useNavigate();
const [isBlocked, setIsBlocked] = useState(false);
const [internalNavigation, setInternalNavigation] = useState(false);
function validate() {
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
}
function canBack() {
return !!window.history && window.history?.length !== 0;
return internalNavigation && !!window.history && window.history?.length !== 0;
}
function push(props: NavigationProps) {
@ -50,6 +51,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
window.open(`${props.path}`, '_blank');
} else if (props.force || validate()) {
setIsBlocked(false);
setInternalNavigation(true);
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
}
}
@ -59,6 +61,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
window.open(`${props.path}`, '_blank');
} else if (props.force || validate()) {
setIsBlocked(false);
setInternalNavigation(true);
return router(props.path, { viewTransition: true });
}
}

View File

@ -1,10 +1,6 @@
import { expect, test } from '@playwright/test';
import { authAnonymous } from './mocks/auth';
import { expect, test } from './setup';
test('should load the homepage and display login button', async ({ page }) => {
await authAnonymous(page);
await page.goto('/');
await expect(page).toHaveTitle('Концепт Портал');

View File

@ -0,0 +1,25 @@
import { setupLogin } from './mocks/auth';
import { expect, test } from './setup';
test('should display error message when login with wrong credentials', async ({ page }) => {
await setupLogin(page);
await page.goto('/login');
await page.getByRole('textbox', { name: 'Логин или email' }).fill('123');
await page.getByRole('textbox', { name: 'Пароль' }).fill('123');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(page.getByText('На Портале отсутствует такое сочетание имени пользователя и пароля')).toBeVisible();
await expect(page.getByRole('button', { name: 'Войти' })).toBeEnabled();
});
test('should login as admin successfully', async ({ page }) => {
await setupLogin(page);
await page.goto('/login');
await page.getByRole('textbox', { name: 'Логин или email' }).fill('admin');
await page.getByRole('textbox', { name: 'Пароль' }).fill('password');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(page.getByText('Вы вошли в систему как admin')).toBeVisible();
await expect(page.getByRole('button', { name: 'Войти' })).toHaveCount(0);
});

View File

@ -1,7 +1,7 @@
import { type Page } from '@playwright/test';
import { type ICurrentUser } from '../../src/features/auth/backend/types';
import { BACKEND_URL } from '../constants';
import { type ICurrentUser, IUserLoginDTO } from '../../src/features/auth/backend/types';
import { BACKEND_URL } from './constants';
const dataAnonymousAuth: ICurrentUser = {
id: null,
@ -24,20 +24,50 @@ const dataUserAuth: ICurrentUser = {
editor: [2]
};
export async function authAnonymous(page: Page) {
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
await route.fulfill({ json: dataAnonymousAuth });
let currentAuth = dataAnonymousAuth;
export async function setupAuth(context: Page) {
await context.route(`${BACKEND_URL}/users/api/auth`, async route => {
await route.fulfill({ json: currentAuth });
});
}
export async function authAdmin(page: Page) {
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
await route.fulfill({ json: dataAdminAuth });
export function authAnonymous() {
currentAuth = dataAnonymousAuth;
}
export function authAdmin() {
currentAuth = dataAdminAuth;
}
export function authUser() {
currentAuth = dataUserAuth;
}
export async function setupLogin(page: Page) {
await page.route(`${BACKEND_URL}/users/api/login`, async route => {
const data = route.request().postDataJSON() as IUserLoginDTO;
if (data.password !== 'password') {
await route.fulfill({
status: 400,
json: {
detail: 'Invalid credentials'
}
});
return;
}
if (data.username === 'admin') {
authAdmin();
} else {
authUser();
}
await route.fulfill({ status: 200 });
});
}
export async function authUser(page: Page) {
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
await route.fulfill({ json: dataUserAuth });
export async function setupLogout(page: Page) {
await page.route(`${BACKEND_URL}/users/api/logout`, async route => {
authAnonymous();
await route.fulfill({ status: 200 });
});
}

View File

@ -6,13 +6,13 @@ import {
type IUserProfile,
type IUserSignupDTO
} from '../../src/features/users/backend/types';
import { BACKEND_URL } from '../constants';
import { BACKEND_URL } from './constants';
const dataActiveUsers: IUserInfo[] = [
{
id: 1,
first_name: 'Admin',
last_name: 'User'
last_name: 'Admin'
},
{
id: 2,
@ -23,10 +23,10 @@ const dataActiveUsers: IUserInfo[] = [
let dataUserProfile: IUserProfile = {
id: 1,
username: 'user',
email: 'user@example.com',
first_name: 'User',
last_name: 'User'
username: 'admin',
email: 'admin@example.com',
first_name: 'Admin',
last_name: 'Admin'
};
export async function setupUsers(page: Page) {

View File

@ -0,0 +1,14 @@
import { test as base } from '@playwright/test';
import { setupAuth } from './mocks/auth';
import { setupUsers } from './mocks/users';
export { expect } from '@playwright/test';
export const test = base.extend({
page: async ({ page }, use) => {
await setupAuth(page);
await setupUsers(page);
await use(page);
}
});

View File

@ -15,7 +15,7 @@
"paths": {
"@/*": ["src/*"]
},
"types": ["playwright"]
"types": ["playwright", "node"]
},
"include": ["playwright.config.ts", "tests/**/*.ts"]
}