From 1c904f965c680775593b4e98a874f613f266d841 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:42:19 +0300 Subject: [PATCH] R: Setup testing environment and mocks --- .github/workflows/frontend.yml | 1 + .vscode/settings.json | 25 ++++++- README.md | 1 + rsconcept/backend/apps/users/serializers.py | 12 ---- rsconcept/frontend/README.md | 21 ------ rsconcept/frontend/eslint.config.js | 21 ++++-- .../frontend/eslint.playwright.config.js | 40 +++++++++++ rsconcept/frontend/package-lock.json | 49 +++++++++++++ rsconcept/frontend/package.json | 4 +- rsconcept/frontend/tests/app.spec.ts | 3 +- rsconcept/frontend/tests/mocks/auth.ts | 36 +++++++++- rsconcept/frontend/tests/mocks/users.ts | 72 +++++++++++++++++++ rsconcept/frontend/tsconfig.json | 2 +- rsconcept/frontend/tsconfig.playwright.json | 21 ++++++ ...{tsconfig.node.json => tsconfig.vite.json} | 2 +- scripts/dev/RunLint.ps1 | 1 + 16 files changed, 265 insertions(+), 46 deletions(-) delete mode 100644 rsconcept/frontend/README.md create mode 100644 rsconcept/frontend/eslint.playwright.config.js create mode 100644 rsconcept/frontend/tests/mocks/users.ts create mode 100644 rsconcept/frontend/tsconfig.playwright.json rename rsconcept/frontend/{tsconfig.node.json => tsconfig.vite.json} (73%) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index fc2dd0bd..46c6e9d0 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -41,6 +41,7 @@ jobs: - name: Run CI run: | npm run lint + npm run lint:playwright npm test - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 6de2fcff..a4782cac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,30 @@ ".pytest_cache/": true }, "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": [ "--line-length", "100", diff --git a/README.md b/README.md index 5fb8fb12..1487e40d 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ This readme file is used mostly to document project dependencies and conventions - eslint-plugin-simple-import-sort - eslint-plugin-react-hooks - eslint-plugin-tsdoc + - eslint-plugin-playwright - babel-plugin-react-compiler - vite - jest diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index 071f058c..1c546be3 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -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): ''' Serializer: User data. ''' id = serializers.IntegerField(read_only=True) diff --git a/rsconcept/frontend/README.md b/rsconcept/frontend/README.md deleted file mode 100644 index 58e2556c..00000000 --- a/rsconcept/frontend/README.md +++ /dev/null @@ -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 - -
-clsx className grouping and order -
-  - 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: 
-  
-
diff --git a/rsconcept/frontend/eslint.config.js b/rsconcept/frontend/eslint.config.js index 7c9a475d..7e58f2b8 100644 --- a/rsconcept/frontend/eslint.config.js +++ b/rsconcept/frontend/eslint.config.js @@ -11,7 +11,16 @@ export default [ ...typescriptPlugin.configs.recommendedTypeChecked, ...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: { @@ -20,7 +29,7 @@ export default [ ecmaVersion: 'latest', sourceType: 'module', globals: { ...globals.browser, ...globals.es2020, ...globals.jest }, - project: ['./tsconfig.json', './tsconfig.node.json'], + project: ['./tsconfig.json', './tsconfig.vite.json'], projectService: true } } @@ -36,6 +45,8 @@ export default [ settings: { react: { version: 'detect' } }, rules: { 'react-compiler/react-compiler': 'error', + 'react-refresh/only-export-components': ['off', { allowConstantExport: true }], + '@typescript-eslint/consistent-type-imports': [ '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': [ 'warn', { @@ -82,8 +93,6 @@ export default [ ] } ], - 'simple-import-sort/exports': 'error', - 'import/no-duplicates': 'warn', ...reactHooksPlugin.configs.recommended.rules } diff --git a/rsconcept/frontend/eslint.playwright.config.js b/rsconcept/frontend/eslint.playwright.config.js new file mode 100644 index 00000000..cca609a2 --- /dev/null +++ b/rsconcept/frontend/eslint.playwright.config.js @@ -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'] + } +]; diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json index c39b3a1b..98a83bee 100644 --- a/rsconcept/frontend/package-lock.json +++ b/rsconcept/frontend/package-lock.json @@ -53,6 +53,7 @@ "babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "eslint": "^9.21.0", "eslint-plugin-import": "^2.31.0", + "eslint-plugin-playwright": "^2.2.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "eslint-plugin-react-hooks": "^5.1.0", @@ -5738,6 +5739,54 @@ "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": { "version": "7.37.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index 793b28a3..2ac0b33b 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -9,7 +9,8 @@ "teste2e": "playwright test", "dev": "vite --host", "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", "preview": "vite preview --port 3000" }, @@ -59,6 +60,7 @@ "babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "eslint": "^9.21.0", "eslint-plugin-import": "^2.31.0", + "eslint-plugin-playwright": "^2.2.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "eslint-plugin-react-hooks": "^5.1.0", diff --git a/rsconcept/frontend/tests/app.spec.ts b/rsconcept/frontend/tests/app.spec.ts index a29b9162..7ec14360 100644 --- a/rsconcept/frontend/tests/app.spec.ts +++ b/rsconcept/frontend/tests/app.spec.ts @@ -6,9 +6,8 @@ test('should load the homepage and display login button', async ({ page }) => { await authAnonymous(page); await page.goto('/'); - await page.waitForSelector('.h-full > .mr-1'); 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(); }); diff --git a/rsconcept/frontend/tests/mocks/auth.ts b/rsconcept/frontend/tests/mocks/auth.ts index 433047bd..9839cf1e 100644 --- a/rsconcept/frontend/tests/mocks/auth.ts +++ b/rsconcept/frontend/tests/mocks/auth.ts @@ -1,9 +1,43 @@ import { type Page } from '@playwright/test'; +import { type ICurrentUser } from '../../src/features/auth/backend/types'; 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) { 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 }); }); } diff --git a/rsconcept/frontend/tests/mocks/users.ts b/rsconcept/frontend/tests/mocks/users.ts new file mode 100644 index 00000000..371f2f23 --- /dev/null +++ b/rsconcept/frontend/tests/mocks/users.ts @@ -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 }); + }); +} diff --git a/rsconcept/frontend/tsconfig.json b/rsconcept/frontend/tsconfig.json index 3ace1996..6a2810eb 100644 --- a/rsconcept/frontend/tsconfig.json +++ b/rsconcept/frontend/tsconfig.json @@ -28,5 +28,5 @@ }, "types": ["vite/client"], "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + "references": [{ "path": "./tsconfig.vite.json" }] } diff --git a/rsconcept/frontend/tsconfig.playwright.json b/rsconcept/frontend/tsconfig.playwright.json new file mode 100644 index 00000000..4adfd0dd --- /dev/null +++ b/rsconcept/frontend/tsconfig.playwright.json @@ -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"] +} diff --git a/rsconcept/frontend/tsconfig.node.json b/rsconcept/frontend/tsconfig.vite.json similarity index 73% rename from rsconcept/frontend/tsconfig.node.json rename to rsconcept/frontend/tsconfig.vite.json index 1febc5ec..e718abca 100644 --- a/rsconcept/frontend/tsconfig.node.json +++ b/rsconcept/frontend/tsconfig.vite.json @@ -11,5 +11,5 @@ "@/*": ["*"] } }, - "include": ["vite.config.ts", "package.json", "playwright.config.ts", "tests", "src"] + "include": ["vite.config.ts", "package.json"] } diff --git a/scripts/dev/RunLint.ps1 b/scripts/dev/RunLint.ps1 index 567e5252..fbd619bf 100644 --- a/scripts/dev/RunLint.ps1 +++ b/scripts/dev/RunLint.ps1 @@ -21,6 +21,7 @@ function LintBackend() { function LintFrontend() { Set-Location $frontend & npm run lint + & npm run lint:playwright } RunLinters \ No newline at end of file