From 7252290416b866289a626868b58777a539a18044 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Thu, 6 Mar 2025 21:09:44 +0300 Subject: [PATCH] R: Add e2e tests for login --- .github/workflows/frontend.yml | 1 + .vscode/launch.json | 21 +++++++- rsconcept/frontend/eslint.config.js | 13 +++-- rsconcept/frontend/playwright.config.ts | 2 +- .../src/app/Navigation/NavigationContext.tsx | 5 +- rsconcept/frontend/tests/app.spec.ts | 6 +-- rsconcept/frontend/tests/auth.spec.ts | 25 +++++++++ rsconcept/frontend/tests/mocks/auth.ts | 52 +++++++++++++++---- .../frontend/tests/{ => mocks}/constants.ts | 0 rsconcept/frontend/tests/mocks/users.ts | 12 ++--- rsconcept/frontend/tests/setup.ts | 14 +++++ rsconcept/frontend/tsconfig.playwright.json | 2 +- 12 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 rsconcept/frontend/tests/auth.spec.ts rename rsconcept/frontend/tests/{ => mocks}/constants.ts (100%) create mode 100644 rsconcept/frontend/tests/setup.ts diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index fc2dd0bd..95c2d772 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -42,6 +42,7 @@ jobs: run: | npm run lint npm test + npm run teste2e - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: diff --git a/.vscode/launch.json b/.vscode/launch.json index 30075f4c..36cf997f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/rsconcept/frontend/eslint.config.js b/rsconcept/frontend/eslint.config.js index ae1e831a..ee457525 100644 --- a/rsconcept/frontend/eslint.config.js +++ b/rsconcept/frontend/eslint.config.js @@ -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' - } } ]; diff --git a/rsconcept/frontend/playwright.config.ts b/rsconcept/frontend/playwright.config.ts index bd160f93..66e16690 100644 --- a/rsconcept/frontend/playwright.config.ts +++ b/rsconcept/frontend/playwright.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ }, webServer: { command: 'npm run dev', - url: 'http://localhost:3000', + port: 3000, reuseExistingServer: !process.env.CI } }); diff --git a/rsconcept/frontend/src/app/Navigation/NavigationContext.tsx b/rsconcept/frontend/src/app/Navigation/NavigationContext.tsx index 752e4fe4..d90b57a3 100644 --- a/rsconcept/frontend/src/app/Navigation/NavigationContext.tsx +++ b/rsconcept/frontend/src/app/Navigation/NavigationContext.tsx @@ -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 }); } } diff --git a/rsconcept/frontend/tests/app.spec.ts b/rsconcept/frontend/tests/app.spec.ts index 7ec14360..8f3376a8 100644 --- a/rsconcept/frontend/tests/app.spec.ts +++ b/rsconcept/frontend/tests/app.spec.ts @@ -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('Концепт Портал'); diff --git a/rsconcept/frontend/tests/auth.spec.ts b/rsconcept/frontend/tests/auth.spec.ts new file mode 100644 index 00000000..b23fc319 --- /dev/null +++ b/rsconcept/frontend/tests/auth.spec.ts @@ -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); +}); diff --git a/rsconcept/frontend/tests/mocks/auth.ts b/rsconcept/frontend/tests/mocks/auth.ts index 9839cf1e..4f0ecd6d 100644 --- a/rsconcept/frontend/tests/mocks/auth.ts +++ b/rsconcept/frontend/tests/mocks/auth.ts @@ -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 }); }); } diff --git a/rsconcept/frontend/tests/constants.ts b/rsconcept/frontend/tests/mocks/constants.ts similarity index 100% rename from rsconcept/frontend/tests/constants.ts rename to rsconcept/frontend/tests/mocks/constants.ts diff --git a/rsconcept/frontend/tests/mocks/users.ts b/rsconcept/frontend/tests/mocks/users.ts index 371f2f23..83556180 100644 --- a/rsconcept/frontend/tests/mocks/users.ts +++ b/rsconcept/frontend/tests/mocks/users.ts @@ -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) { diff --git a/rsconcept/frontend/tests/setup.ts b/rsconcept/frontend/tests/setup.ts new file mode 100644 index 00000000..0c45260b --- /dev/null +++ b/rsconcept/frontend/tests/setup.ts @@ -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); + } +}); diff --git a/rsconcept/frontend/tsconfig.playwright.json b/rsconcept/frontend/tsconfig.playwright.json index 4adfd0dd..28877f01 100644 --- a/rsconcept/frontend/tsconfig.playwright.json +++ b/rsconcept/frontend/tsconfig.playwright.json @@ -15,7 +15,7 @@ "paths": { "@/*": ["src/*"] }, - "types": ["playwright"] + "types": ["playwright", "node"] }, "include": ["playwright.config.ts", "tests/**/*.ts"] }