R: Setup testing environment and mocks
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run

This commit is contained in:
Ivan 2025-03-02 19:42:19 +03:00
parent 32f83c4122
commit 1c904f965c
16 changed files with 265 additions and 46 deletions

View File

@ -41,6 +41,7 @@ jobs:
- name: Run CI - name: Run CI
run: | run: |
npm run lint npm run lint
npm run lint:playwright
npm test npm test
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}

25
.vscode/settings.json vendored
View File

@ -4,7 +4,30 @@
".pytest_cache/": true ".pytest_cache/": true
}, },
"typescript.tsdk": "rsconcept/frontend/node_modules/typescript/lib", "typescript.tsdk": "rsconcept/frontend/node_modules/typescript/lib",
"eslint.workingDirectories": ["rsconcept/frontend"], "eslint.probe": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.workingDirectories": [
{
"directory": "./",
"overrideConfigFile": "eslint.config.js",
"changeProcessCWD": true
},
{
"directory": "./",
"pattern": "tests/**",
"overrideConfigFile": "eslint.playwright.config.js"
}
],
"isort.args": [ "isort.args": [
"--line-length", "--line-length",
"100", "100",

View File

@ -66,6 +66,7 @@ This readme file is used mostly to document project dependencies and conventions
- eslint-plugin-simple-import-sort - eslint-plugin-simple-import-sort
- eslint-plugin-react-hooks - eslint-plugin-react-hooks
- eslint-plugin-tsdoc - eslint-plugin-tsdoc
- eslint-plugin-playwright
- babel-plugin-react-compiler - babel-plugin-react-compiler
- vite - vite
- jest - jest

View File

@ -77,18 +77,6 @@ class AuthSerializer(serializers.Serializer):
} }
class UserInfoSerializer(serializers.ModelSerializer):
''' Serializer: User data. '''
class Meta:
''' serializer metadata. '''
model = models.User
fields = [
'id',
'first_name',
'last_name',
]
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
''' Serializer: User data. ''' ''' Serializer: User data. '''
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)

View File

@ -1,21 +0,0 @@
# Frontend Developer guidelines
Styling conventions
- static > conditional static > props. All dynamic styling should go in styles props
- dimensions = rectangle + outer layout
<details>
<summary>clsx className grouping and order</summary>
<pre>
- layer: z-position
- outer layout: fixed bottom-1/2 left-0 -translate-x-1/2
- rectangle: mt-3 min-w-fit min-w-10 flex-grow shrink-0
- inner layout: px-3 py-2 flex flex-col gap-3 justify-between items-center
- overflow behavior: overflow-scroll overscroll-contain
- border: borer-2 outline-none shadow-md
- text: text-start text-sm font-semibold whitespace-nowrap bg-prim-200 fg-app-100
- behavior modifiers: select-none disabled:cursor-auto
- transitions:
</pre>
</details>

View File

@ -11,7 +11,16 @@ export default [
...typescriptPlugin.configs.recommendedTypeChecked, ...typescriptPlugin.configs.recommendedTypeChecked,
...typescriptPlugin.configs.stylisticTypeChecked, ...typescriptPlugin.configs.stylisticTypeChecked,
{ {
ignores: ['**/parser.ts', '**/node_modules/**', '**/public/**', '**/dist/**', 'eslint.config.js'] ignores: [
'**/parser.ts',
'**/node_modules/**',
'**/public/**',
'**/dist/**',
'eslint.config.js',
'playwright.config.ts',
'eslint.playwright.config.js',
'tests/**'
]
}, },
{ {
languageOptions: { languageOptions: {
@ -20,7 +29,7 @@ export default [
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
globals: { ...globals.browser, ...globals.es2020, ...globals.jest }, globals: { ...globals.browser, ...globals.es2020, ...globals.jest },
project: ['./tsconfig.json', './tsconfig.node.json'], project: ['./tsconfig.json', './tsconfig.vite.json'],
projectService: true projectService: true
} }
} }
@ -36,6 +45,8 @@ export default [
settings: { react: { version: 'detect' } }, settings: { react: { version: 'detect' } },
rules: { rules: {
'react-compiler/react-compiler': 'error', 'react-compiler/react-compiler': 'error',
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
'@typescript-eslint/consistent-type-imports': [ '@typescript-eslint/consistent-type-imports': [
'warn', 'warn',
{ {
@ -55,8 +66,8 @@ export default [
} }
], ],
'react-refresh/only-export-components': ['off', { allowConstantExport: true }], 'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
'simple-import-sort/imports': [ 'simple-import-sort/imports': [
'warn', 'warn',
{ {
@ -82,8 +93,6 @@ export default [
] ]
} }
], ],
'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
...reactHooksPlugin.configs.recommended.rules ...reactHooksPlugin.configs.recommended.rules
} }

View File

@ -0,0 +1,40 @@
import globals from 'globals';
import typescriptParser from '@typescript-eslint/parser';
import playwright from 'eslint-plugin-playwright';
import importPlugin from 'eslint-plugin-import';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
export default [
{
...playwright.configs['flat/recommended'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2020
}
}
},
plugins: {
'playwright': playwright,
'simple-import-sort': simpleImportSort,
'import': importPlugin
},
rules: {
...playwright.configs['flat/recommended'].rules,
'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
'simple-import-sort/imports': 'warn'
},
files: ['tests/**/*.ts']
}
];

View File

@ -53,6 +53,7 @@
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint": "^9.21.0", "eslint": "^9.21.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
@ -5738,6 +5739,54 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/eslint-plugin-playwright": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.2.0.tgz",
"integrity": "sha512-qSQpAw7RcSzE3zPp8FMGkthaCWovHZ/BsXtpmnGax9vQLIovlh1bsZHEa2+j2lv9DWhnyeLM/qZmp7ffQZfQvg==",
"dev": true,
"license": "MIT",
"workspaces": [
"examples"
],
"dependencies": {
"globals": "^13.23.0"
},
"engines": {
"node": ">=16.6.0"
},
"peerDependencies": {
"eslint": ">=8.40.0"
}
},
"node_modules/eslint-plugin-playwright/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-playwright/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.37.4", "version": "7.37.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz",

View File

@ -9,7 +9,8 @@
"teste2e": "playwright test", "teste2e": "playwright test",
"dev": "vite --host", "dev": "vite --host",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --config eslint.config.js --report-unused-disable-directives --max-warnings 0",
"lint:playwright": "eslint . --config eslint.playwright.config.js --report-unused-disable-directives --max-warnings 0",
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix", "lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
"preview": "vite preview --port 3000" "preview": "vite preview --port 3000"
}, },
@ -59,6 +60,7 @@
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint": "^9.21.0", "eslint": "^9.21.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",

View File

@ -6,9 +6,8 @@ test('should load the homepage and display login button', async ({ page }) => {
await authAnonymous(page); await authAnonymous(page);
await page.goto('/'); await page.goto('/');
await page.waitForSelector('.h-full > .mr-1');
await expect(page).toHaveTitle('Концепт Портал'); await expect(page).toHaveTitle('Концепт Портал');
await page.click('.h-full > .mr-1'); await page.locator('.h-full > .mr-1').click();
await expect(page.getByText('Логин или email')).toBeVisible(); await expect(page.getByText('Логин или email')).toBeVisible();
}); });

View File

@ -1,9 +1,43 @@
import { type Page } from '@playwright/test'; import { type Page } from '@playwright/test';
import { type ICurrentUser } from '../../src/features/auth/backend/types';
import { BACKEND_URL } from '../constants'; import { BACKEND_URL } from '../constants';
const dataAnonymousAuth: ICurrentUser = {
id: null,
username: '',
is_staff: false,
editor: []
};
const dataAdminAuth: ICurrentUser = {
id: 1,
username: 'admin',
is_staff: true,
editor: []
};
const dataUserAuth: ICurrentUser = {
id: 2,
username: 'user',
is_staff: false,
editor: [2]
};
export async function authAnonymous(page: Page) { export async function authAnonymous(page: Page) {
await page.route(`${BACKEND_URL}/users/api/auth`, async route => { await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
await route.fulfill({ json: { id: null } }); await route.fulfill({ json: dataAnonymousAuth });
});
}
export async function authAdmin(page: Page) {
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
await route.fulfill({ json: dataAdminAuth });
});
}
export async function authUser(page: Page) {
await page.route(`${BACKEND_URL}/users/api/auth`, async route => {
await route.fulfill({ json: dataUserAuth });
}); });
} }

View File

@ -0,0 +1,72 @@
import { type Page } from '@playwright/test';
import {
type IUpdateProfileDTO,
type IUserInfo,
type IUserProfile,
type IUserSignupDTO
} from '../../src/features/users/backend/types';
import { BACKEND_URL } from '../constants';
const dataActiveUsers: IUserInfo[] = [
{
id: 1,
first_name: 'Admin',
last_name: 'User'
},
{
id: 2,
first_name: 'User',
last_name: 'User'
}
];
let dataUserProfile: IUserProfile = {
id: 1,
username: 'user',
email: 'user@example.com',
first_name: 'User',
last_name: 'User'
};
export async function setupUsers(page: Page) {
await page.route(`${BACKEND_URL}/users/api/active-users`, async route => {
await route.fulfill({ json: dataActiveUsers });
});
}
export async function setupUserProfile(page: Page) {
await page.route(`${BACKEND_URL}/users/api/profile`, async route => {
await route.fulfill({ json: dataUserProfile });
});
}
export async function setupUserSignup(page: Page) {
await page.route(`${BACKEND_URL}/users/api/signup`, async route => {
const data = route.request().postDataJSON() as IUserSignupDTO;
const newID = dataActiveUsers.length + 1;
dataActiveUsers.push({
id: newID,
first_name: data.first_name,
last_name: data.last_name
});
dataUserProfile = {
id: newID,
username: data.username,
email: data.email,
first_name: data.first_name,
last_name: data.last_name
};
await route.fulfill({ json: dataUserProfile });
});
}
export async function setupUserProfileUpdate(page: Page) {
await page.route(`${BACKEND_URL}/users/api/profile`, async route => {
dataUserProfile = {
...dataUserProfile,
...(route.request().postDataJSON() as IUpdateProfileDTO)
};
await route.fulfill({ json: dataUserProfile });
});
}

View File

@ -28,5 +28,5 @@
}, },
"types": ["vite/client"], "types": ["vite/client"],
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.vite.json" }]
} }

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "dom"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"isolatedModules": false,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"types": ["playwright"]
},
"include": ["playwright.config.ts", "tests/**/*.ts"]
}

View File

@ -11,5 +11,5 @@
"@/*": ["*"] "@/*": ["*"]
} }
}, },
"include": ["vite.config.ts", "package.json", "playwright.config.ts", "tests", "src"] "include": ["vite.config.ts", "package.json"]
} }

View File

@ -21,6 +21,7 @@ function LintBackend() {
function LintFrontend() { function LintFrontend() {
Set-Location $frontend Set-Location $frontend
& npm run lint & npm run lint
& npm run lint:playwright
} }
RunLinters RunLinters