mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-25 20:40:36 +03:00
R: Add e2e tests for login
This commit is contained in:
parent
31c32d5983
commit
7252290416
1
.github/workflows/frontend.yml
vendored
1
.github/workflows/frontend.yml
vendored
|
@ -42,6 +42,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
npm run lint
|
npm run lint
|
||||||
npm test
|
npm test
|
||||||
|
npm run teste2e
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
|
|
21
.vscode/launch.json
vendored
21
.vscode/launch.json
vendored
|
@ -46,9 +46,28 @@
|
||||||
"args": ["test", "-k", "${fileBasenameNoExtension}"],
|
"args": ["test", "-k", "${fileBasenameNoExtension}"],
|
||||||
"django": true
|
"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
|
// Run Tests for frontned in Debug mode
|
||||||
"name": "FE-DebugTestAll",
|
"name": "Jest DebugAll",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest",
|
"runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest",
|
||||||
|
|
|
@ -45,6 +45,9 @@ export default [
|
||||||
},
|
},
|
||||||
settings: { react: { version: 'detect' } },
|
settings: { react: { version: 'detect' } },
|
||||||
rules: {
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'require-jsdoc': 'off',
|
||||||
|
|
||||||
'react-compiler/react-compiler': 'error',
|
'react-compiler/react-compiler': 'error',
|
||||||
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
||||||
|
|
||||||
|
@ -112,16 +115,12 @@ export default [
|
||||||
rules: {
|
rules: {
|
||||||
...playwright.configs['flat/recommended'].rules,
|
...playwright.configs['flat/recommended'].rules,
|
||||||
|
|
||||||
|
'no-console': 'off',
|
||||||
|
'require-jsdoc': 'off',
|
||||||
|
|
||||||
'simple-import-sort/exports': 'error',
|
'simple-import-sort/exports': 'error',
|
||||||
'import/no-duplicates': 'warn',
|
'import/no-duplicates': 'warn',
|
||||||
'simple-import-sort/imports': 'warn'
|
'simple-import-sort/imports': 'warn'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
|
||||||
rules: {
|
|
||||||
'no-console': 'off',
|
|
||||||
'require-jsdoc': 'off'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run dev',
|
command: 'npm run dev',
|
||||||
url: 'http://localhost:3000',
|
port: 3000,
|
||||||
reuseExistingServer: !process.env.CI
|
reuseExistingServer: !process.env.CI
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,13 +36,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
|
|
||||||
const [isBlocked, setIsBlocked] = useState(false);
|
const [isBlocked, setIsBlocked] = useState(false);
|
||||||
|
const [internalNavigation, setInternalNavigation] = useState(false);
|
||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
||||||
}
|
}
|
||||||
|
|
||||||
function canBack() {
|
function canBack() {
|
||||||
return !!window.history && window.history?.length !== 0;
|
return internalNavigation && !!window.history && window.history?.length !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function push(props: NavigationProps) {
|
function push(props: NavigationProps) {
|
||||||
|
@ -50,6 +51,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
window.open(`${props.path}`, '_blank');
|
window.open(`${props.path}`, '_blank');
|
||||||
} else if (props.force || validate()) {
|
} else if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
setInternalNavigation(true);
|
||||||
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
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');
|
window.open(`${props.path}`, '_blank');
|
||||||
} else if (props.force || validate()) {
|
} else if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
setInternalNavigation(true);
|
||||||
return router(props.path, { viewTransition: true });
|
return router(props.path, { viewTransition: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from './setup';
|
||||||
|
|
||||||
import { authAnonymous } from './mocks/auth';
|
|
||||||
|
|
||||||
test('should load the homepage and display login button', async ({ page }) => {
|
test('should load the homepage and display login button', async ({ page }) => {
|
||||||
await authAnonymous(page);
|
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle('Концепт Портал');
|
await expect(page).toHaveTitle('Концепт Портал');
|
||||||
|
|
||||||
|
|
25
rsconcept/frontend/tests/auth.spec.ts
Normal file
25
rsconcept/frontend/tests/auth.spec.ts
Normal 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);
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import { type Page } from '@playwright/test';
|
import { type Page } from '@playwright/test';
|
||||||
|
|
||||||
import { type ICurrentUser } from '../../src/features/auth/backend/types';
|
import { type ICurrentUser, IUserLoginDTO } from '../../src/features/auth/backend/types';
|
||||||
import { BACKEND_URL } from '../constants';
|
import { BACKEND_URL } from './constants';
|
||||||
|
|
||||||
const dataAnonymousAuth: ICurrentUser = {
|
const dataAnonymousAuth: ICurrentUser = {
|
||||||
id: null,
|
id: null,
|
||||||
|
@ -24,20 +24,50 @@ const dataUserAuth: ICurrentUser = {
|
||||||
editor: [2]
|
editor: [2]
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function authAnonymous(page: Page) {
|
let currentAuth = dataAnonymousAuth;
|
||||||
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
|
|
||||||
await route.fulfill({ json: 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) {
|
export function authAnonymous() {
|
||||||
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
|
currentAuth = dataAnonymousAuth;
|
||||||
await route.fulfill({ json: dataAdminAuth });
|
}
|
||||||
|
|
||||||
|
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) {
|
export async function setupLogout(page: Page) {
|
||||||
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
|
await page.route(`${BACKEND_URL}/users/api/logout`, async route => {
|
||||||
await route.fulfill({ json: dataUserAuth });
|
authAnonymous();
|
||||||
|
await route.fulfill({ status: 200 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,13 @@ import {
|
||||||
type IUserProfile,
|
type IUserProfile,
|
||||||
type IUserSignupDTO
|
type IUserSignupDTO
|
||||||
} from '../../src/features/users/backend/types';
|
} from '../../src/features/users/backend/types';
|
||||||
import { BACKEND_URL } from '../constants';
|
import { BACKEND_URL } from './constants';
|
||||||
|
|
||||||
const dataActiveUsers: IUserInfo[] = [
|
const dataActiveUsers: IUserInfo[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
first_name: 'Admin',
|
first_name: 'Admin',
|
||||||
last_name: 'User'
|
last_name: 'Admin'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
|
@ -23,10 +23,10 @@ const dataActiveUsers: IUserInfo[] = [
|
||||||
|
|
||||||
let dataUserProfile: IUserProfile = {
|
let dataUserProfile: IUserProfile = {
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'user',
|
username: 'admin',
|
||||||
email: 'user@example.com',
|
email: 'admin@example.com',
|
||||||
first_name: 'User',
|
first_name: 'Admin',
|
||||||
last_name: 'User'
|
last_name: 'Admin'
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function setupUsers(page: Page) {
|
export async function setupUsers(page: Page) {
|
||||||
|
|
14
rsconcept/frontend/tests/setup.ts
Normal file
14
rsconcept/frontend/tests/setup.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
|
@ -15,7 +15,7 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"types": ["playwright"]
|
"types": ["playwright", "node"]
|
||||||
},
|
},
|
||||||
"include": ["playwright.config.ts", "tests/**/*.ts"]
|
"include": ["playwright.config.ts", "tests/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user