Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/nodeci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,13 @@ jobs:

- name: Install
run: make install
- name: Build frontend
run: make build
- name: Run linter
run: make lint
# Браузерные тесты это единственная проверка интерфейса в репозитории:
# юнит-тестов тут нет.
- name: Install browsers
run: npx playwright install --with-deps chromium
- name: Run tests
run: make test
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/coverage

# production
/build
dist/

# misc
.DS_Store
Expand All @@ -21,3 +21,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
tmp/
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ lint-frontend:
make -C frontend lint

build:
DISABLE_ESLINT_PLUGIN=true pnpm run build
pnpm run build

test:
npx playwright test


install:
Expand Down
22 changes: 11 additions & 11 deletions __tests__/example.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,38 @@ const { test, expect } = require("@playwright/test");
test("wrong login", async ({ page }) => {
await page.goto("http://localhost:5001/");

expect(page.getByText(/Нет аккаунта?/)).toBeVisible();
expect(page.getByAltText(/Войти/)).toBeVisible();
expect(page.getByText(/Регистрация/)).toBeVisible();
await expect(page.getByText(/Нет аккаунта?/)).toBeVisible();
await expect(page.getByAltText(/Войти/)).toBeVisible();
await expect(page.getByText(/Регистрация/)).toBeVisible();
expect(await page.getByText(/Войти/).count()).toEqual(2);

await page.locator("#username").fill("test");
await page.locator("#password").fill("test");
await page.getByRole("button", { name: "Войти" }).click();
expect(await page.getByText(/Скрытая страница!/).count()).toEqual(0);
expect(page.getByText(/Неверные имя пользователя или пароль/)).toBeVisible();
await expect(page.getByText(/Неверные имя пользователя или пароль/)).toBeVisible();
});

test("registration page, success login", async ({ page }) => {
await page.goto("http://localhost:5001/");

await page.getByText(/Регистрация/).click();
expect(page.getByLabel(/Имя пользователя/)).toBeVisible();
expect(page.getByLabel(/Пароль/)).toBeVisible();
expect(page.getByLabel(/Подтвердите пароль/)).toBeVisible();
expect(page.getByText(/Регистрация/)).toBeVisible();
expect(page.getByAltText(/Регистрация/)).toBeVisible();
await expect(page.getByLabel(/Имя пользователя/)).toBeVisible();
await expect(page.getByLabel(/Пароль/)).toBeVisible();
await expect(page.getByLabel(/Подтвердите пароль/)).toBeVisible();
await expect(page.getByText(/Регистрация/)).toBeVisible();
await expect(page.getByAltText(/Регистрация/)).toBeVisible();

await page.locator("#username").fill("playwright");
await page.locator("#password").fill("password");
await page.locator("#confirmPassword").fill("password");
await page.getByRole("button", { name: "Зарегистрироваться" }).click();
expect(page.getByText(/Скрытая страница!/)).toBeVisible();
await expect(page.getByText(/Скрытая страница!/)).toBeVisible();

await page.getByRole("button", { name: "Выйти" }).click();

await page.locator("#username").fill("admin");
await page.locator("#password").fill("admin");
await page.getByRole("button", { name: "Войти" }).click();
expect(page.getByText(/Скрытая страница!/)).toBeVisible();
await expect(page.getByText(/Скрытая страница!/)).toBeVisible();
});
4 changes: 0 additions & 4 deletions frontend/i18next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,5 @@ export default defineConfig({
output: "src/locales/{{language}}/{{namespace}}.json",
defaultNS: "translation",
primaryLanguage: "ru",
// Сообщения валидации это ключи в схеме yup: их подставляет formik, и в
// t() попадает переменная. Статически такой ключ не виден, поэтому без
// этой строки extract вычистил бы половину раздела signup.
preservePatterns: ["signup.*"],
},
});
16 changes: 16 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Пример React-приложения с авторизацией" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Hexlet React App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
37 changes: 14 additions & 23 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,28 @@
"name": "frontend",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@mantine/core": "^9.5.1",
"@mantine/form": "^9.5.1",
"@mantine/hooks": "^9.5.1",
"@mantine/notifications": "^9.5.1",
"axios": "^0.27.2",
"bootstrap": "^5.1.3",
"formik": "^2.2.9",
"i18next": "^26.3.6",
"react": "^18.2.0",
"react-bootstrap": "^2.4.0",
"react-dom": "^18.2.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.11",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"react-toastify": "^9.0.5",
"sass": "^1.53.0",
"web-vitals": "^2.1.4",
"yup": "^0.32.11"
"react-router-dom": "^6.3.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"dev": "vite",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "oxlint --config=.oxlintrc.json .",
"lint:fix": "oxfmt --ignore-path=.oxfmtignore . && oxlint --config=.oxlintrc.json --fix .",
"format": "oxfmt --ignore-path=.oxfmtignore .",
"format:check": "oxfmt --ignore-path=.oxfmtignore --check ."
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
Expand All @@ -48,8 +37,10 @@
]
},
"devDependencies": {
"@vitejs/plugin-react": "^6.0.5",
"i18next-cli": "^1.69.2",
"oxfmt": "^0.63.0",
"oxlint": "^1.78.0"
"oxlint": "^1.78.0",
"vite": "^8.2.1"
}
}
40 changes: 0 additions & 40 deletions frontend/public/index.html

This file was deleted.

2 changes: 0 additions & 2 deletions frontend/src/assets/application.scss

This file was deleted.

45 changes: 28 additions & 17 deletions frontend/src/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import React, { useState } from "react";
import { BrowserRouter as Router, Route, Routes, Navigate, Outlet } from "react-router-dom";
import { ToastContainer as Toaster } from "react-toastify";
import { AppShell, MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";

import Navbar from "./Navbar.jsx";
import Registration from "./Registration.jsx";
Expand All @@ -14,6 +15,8 @@ import { AuthContext } from "../contexts/index.js";
import { useAuth } from "../hooks/index.js";
import routes from "../routes.js";

const headerHeight = 56;

const AuthProvider = ({ children }) => {
const currentUser = JSON.parse(localStorage.getItem("user"));
const [user, setUser] = useState(currentUser ? { username: currentUser.username } : null);
Expand Down Expand Up @@ -53,22 +56,30 @@ const PrivateOutlet = () => {
};

const App = () => (
<AuthProvider>
<Router>
<div className="d-flex flex-column h-100">
<Navbar />
<Routes>
<Route path={routes.signupPagePath()} element={<Registration />} />
<Route path={routes.loginPagePath()} element={<Login />} />
<Route path={routes.chatPagePath()} element={<PrivateOutlet />}>
<Route path="" element={<PrivatePage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</div>
<Toaster />
</Router>
</AuthProvider>
<MantineProvider>
{/* Правый верхний угол занят шапкой: уведомление там перекрывало бы кнопку
выхода и мешало по ней кликнуть. */}
<Notifications position="bottom-right" />
<AuthProvider>
<Router>
<AppShell header={{ height: headerHeight }} padding="md">
<AppShell.Header>
<Navbar />
</AppShell.Header>
<AppShell.Main>
<Routes>
<Route path={routes.signupPagePath()} element={<Registration />} />
<Route path={routes.loginPagePath()} element={<Login />} />
<Route path={routes.chatPagePath()} element={<PrivateOutlet />}>
<Route path="" element={<PrivatePage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</AppShell.Main>
</AppShell>
</Router>
</AuthProvider>
</MantineProvider>
);

export default App;
Loading