Compare commits

..

28 Commits

Author SHA1 Message Date
RustamRu
351420bc62 0.9.1
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-23 12:37:44 +03:00
RustamRu
61b042eee6 fix: image input accept, i18n (#88)
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-23 12:37:04 +03:00
RustamRu
ac006267a2 0.9.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-23 12:11:20 +03:00
RustamRu
63d9d069c0 feat: add car image feature
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-23 12:08:50 +03:00
c83ebf02bc Merge pull request 'feature/upload-car-image' (#90) from feature/upload-car-image into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #90
2025-02-23 12:02:39 +03:00
RustamRu
1968df7bb3 fix: getOrder test (#88)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-02-23 11:58:56 +03:00
RustamRu
811e0e3f24 Merge remote-tracking branch 'origin/main' into feature/upload-car-image
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2025-02-23 11:47:04 +03:00
a2ffd6f38f Merge pull request 'fix tests error' (#89) from debug into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #89
2025-02-23 11:32:09 +03:00
RustamRu
20017cad3c feat: upload car img form (#88)
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2025-02-22 19:46:27 +03:00
RustamRu
de54ac6669 feat: api upload car img (#88) 2025-02-22 18:56:37 +03:00
RustamRu
eda869622e fix: error body message -> error (#88) 2025-02-22 18:56:20 +03:00
Primakov Alexandr Alexandrovich
7b3889aa02 run last one
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2025-02-20 14:15:25 +03:00
Primakov Alexandr Alexandrovich
cee124fca5 other 2
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-20 14:10:46 +03:00
Primakov Alexandr Alexandrovich
b77eccc8e8 test: улучшены описания тестов страницы просмотра заказа
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
- Добавлены более информативные описания тестовых сценариев
- Улучшена читаемость тестов за счет детальных названий на русском языке
2025-02-20 14:04:32 +03:00
Primakov Alexandr Alexandrovich
ebfaa7ea8f orders page
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-20 14:00:34 +03:00
Primakov Alexandr Alexandrovich
0027cc09b1 masters test
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-20 13:55:05 +03:00
Primakov Alexandr Alexandrovich
dd612d662c debug
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-20 13:44:38 +03:00
Primakov Alexandr Alexandrovich
69251745fa try fix
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2025-02-20 13:32:39 +03:00
Primakov Alexandr Alexandrovich
253e3b3856 try skip one test
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2025-02-20 13:26:35 +03:00
RustamRu
c9c17340c6 Merge branch 'feature/landing-tests'
Some checks failed
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2025-02-16 12:05:31 +03:00
RustamRu
7fc5455c37 0.8.0
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-16 11:57:38 +03:00
RustamRu
24779e2592 fix: orders date format 2025-02-16 11:55:18 +03:00
RustamRu
9111724519 Merge branch 'main' of ssh://85.143.175.152:222/dry_wash_inc/dry-wash-pl
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-16 11:45:55 +03:00
RustamRu
c2511e0917 fix: order view status polling 2025-02-16 11:45:42 +03:00
b7d935f557 Merge pull request 'feature/testArm' (#87) from feature/testArm into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #87
2025-02-16 11:36:51 +03:00
fdc20e7464 feat: detete ts jest
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main Build started...
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2025-02-16 11:36:13 +03:00
a616d3815b feat: add master test
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2025-02-16 11:31:26 +03:00
56f65fbd3a feat: add test for orders 2025-02-16 11:31:05 +03:00
42 changed files with 1346 additions and 276 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"i18n-ally.localesPaths": [
"locales"
]
}

32
Jenkinsfile vendored
View File

@ -1,7 +1,7 @@
pipeline {
agent {
docker {
image 'node:20'
image 'node:22'
}
}
@ -30,25 +30,21 @@ pipeline {
}
}
stage('checks') {
parallel {
stage('eslint') {
steps {
sh 'npm run eslint'
}
}
stage('eslint') {
steps {
sh 'npm run eslint'
}
}
stage('test') {
steps {
sh 'npm run test'
}
}
stage('test') {
steps {
sh 'npm run test'
}
}
stage('build') {
steps {
sh 'npm run build'
}
}
stage('build') {
steps {
sh 'npm run build'
}
}

View File

@ -19,9 +19,18 @@ module.exports = {
'dry-wash.arm': '/arm/*',
},
features: {
'dry-wash-pl': {
'dry-wash': {
// add your features here in the format [featureName]: { value: string }
'order-view-status-polling': { value: '3000' }
"order-view-status-polling": {
"on": true,
"value": "3000",
"key": "order-view-status-polling"
},
"car-img-upload": {
"on": true,
"value": "true",
"key": "car-img-upload"
}
},
},
config: {

View File

@ -6,7 +6,7 @@ test.beforeEach('check server is up', async ({ page }) => {
const makeOrderText = page.getByText('Сделать заказ', { exact: true });
await expect(makeOrderText).toBeVisible();
} catch (error) {
console.error('server not up');
console.error('server not up', error);
test.skip();
}
});

View File

@ -50,6 +50,12 @@
"dry-wash.order-view.details.location": "Where",
"dry-wash.order-view.details.datetime-range": "When",
"dry-wash.order-view.details.alert": "The operator will contact you about the payment at the specified phone number",
"dry-wash.order-view.upload-car-image.field.label": "Upload a photo of your car, and our service will quickly calculate the pre-order price!",
"dry-wash.order-view.upload-car-image.field.help": "Allowed formats: .jpg, .png. Maximum size: 5MB",
"dry-wash.order-view.upload-car-image.file-input.placeholder": "Upload a file",
"dry-wash.order-view.upload-car-image.file-input.button": "Upload",
"dry-wash.order-view.upload-car-image-query.success.title": "The car image is successfully uploaded",
"dry-wash.order-view.upload-car-image-query.error.title": "Failed to upload the car image",
"dry-wash.arm.master.add": "Add",
"dry-wash.arm.order.title": "Orders",
"dry-wash.arm.order.table.empty": "Table empty",

View File

@ -105,6 +105,12 @@
"dry-wash.order-view.details.location": "Где",
"dry-wash.order-view.details.datetime-range": "Когда",
"dry-wash.order-view.details.alert": "С вами свяжется оператор насчет оплаты по указанному номеру телефона",
"dry-wash.order-view.upload-car-image.field.label": "Загрузите фото вашего автомобиля, и наш сервис быстро рассчитает предварительную стоимость заказа!",
"dry-wash.order-view.upload-car-image.field.help": "Допустимые форматы: .jpg, .png. Максимальный размер: 5МБ",
"dry-wash.order-view.upload-car-image.file-input.placeholder": "Загрузите файл",
"dry-wash.order-view.upload-car-image.file-input.button": "Загрузить",
"dry-wash.order-view.upload-car-image-query.success.title": "Изображение автомобиля успешно загружено",
"dry-wash.order-view.upload-car-image-query.error.title": "Не удалось загрузить изображение автомобиля",
"dry-wash.notFound.title": "Страница не найдена",
"dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.",
"dry-wash.notFound.button.back": "Вернуться на главную",

24
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "dry-wash",
"version": "0.7.2",
"version": "0.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dry-wash",
"version": "0.7.2",
"version": "0.9.1",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.26.7",
@ -68,8 +68,7 @@
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz",
"integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
@ -3830,7 +3829,6 @@
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
@ -3851,7 +3849,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@ -3867,7 +3864,6 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@ -3880,15 +3876,13 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/@testing-library/jest-dom/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@ -6086,8 +6080,7 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/cssesc": {
"version": "3.0.0",
@ -8823,7 +8816,6 @@
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@ -11413,8 +11405,7 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@ -11611,7 +11602,6 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
@ -13175,7 +13165,6 @@
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
@ -13993,7 +13982,6 @@
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
},

View File

@ -1,16 +1,16 @@
{
"name": "dry-wash",
"version": "0.7.2",
"version": "0.9.1",
"description": "<a id=\"readme-top\"></a>",
"main": "./src/index.tsx",
"scripts": {
"test": "jest",
"test": "jest -u",
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build",
"clean": "rimraf dist",
"eslint": "npx eslint .",
"eslint:fix": "npx eslint . --fix",
"eslint": "npx eslint src",
"eslint:fix": "npx eslint src --fix",
"preversion": "npm run eslint"
},
"keywords": [],

View File

@ -1,15 +1,18 @@
import { getFeatures } from "@brojs/cli";
const features = getFeatures('dry-wash-pl');
const features = getFeatures('dry-wash');
export const FEATURE = {
orderViewStatusPolling: {
isOn: Boolean(features['order-view-status-polling']),
isOn: Boolean(features?.['order-view-status-polling']),
getValue: () => {
const interval = parseInt(features['order-view-status-polling'].value);
const interval = parseInt(features?.['order-view-status-polling']?.value);
if (!Number.isNaN(interval)) {
return interval;
}
}
},
carImageUpload: {
isOn: Boolean(features?.['car-img-upload'])
}
};

View File

@ -1,4 +1,4 @@
import { GetOrder, CreateOrder } from "../../models/api";
import { GetOrder, CreateOrder, UploadCarImage } from "../../models/api";
import { api } from "./api";
import { extractBodyFromResponse, extractErrorMessageFromResponse } from "./utils";
@ -19,5 +19,13 @@ export const landingApi = api.injectEndpoints({
transformResponse: extractBodyFromResponse<CreateOrder.Response>,
transformErrorResponse: extractErrorMessageFromResponse,
}),
uploadCarImage: mutation<UploadCarImage.Response, UploadCarImage.Params>({
query: ({ orderId, body }) => ({
url: `/order/${orderId}/upload-car-img`,
body,
method: 'POST'
}),
transformErrorResponse: extractErrorMessageFromResponse,
}),
})
});

View File

@ -1,6 +1,6 @@
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
import { BaseResponse } from "../../models/api";
import { BaseResponse } from '../../models/api';
export const extractBodyFromResponse = <Body>(response: BaseResponse<Body>) => {
if (response.success) {
@ -8,8 +8,15 @@ export const extractBodyFromResponse = <Body>(response: BaseResponse<Body>) => {
}
};
export const extractErrorMessageFromResponse = ({ data }: FetchBaseQueryError) => {
if (typeof data === 'object' && 'message' in data && typeof data.message === 'string') {
return data.message;
export const extractErrorMessageFromResponse = ({
data,
}: FetchBaseQueryError) => {
if (
typeof data === 'object' &&
data !== null &&
'error' in data &&
typeof data.error === 'string'
) {
return data.error;
}
};

View File

@ -7,7 +7,9 @@ export const store = configureStore({
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
getDefaultMiddleware({
serializableCheck: false
}).concat(api.middleware),
});
export type RootState = ReturnType<typeof store.getState>;

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Box, Button, Text } from '@chakra-ui/react';
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
import dayjs from 'dayjs';
interface DateNavigatorProps {
currentDate: Date;
@ -19,7 +20,7 @@ const DateNavigator = ({
<ArrowBackIcon />
</Button>
<Text mx='4' fontSize='lg' fontWeight='bold'>
{currentDate.toLocaleDateString()}
{dayjs(currentDate).format('DD.MM.YYYY')}
</Text>
<Button onClick={onNextDate}>
<ArrowForwardIcon />

View File

@ -33,22 +33,22 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
render() {
const { hasError } = this.state;
//TODO: добавить анимацию после залива 404 страницы
//TODO: может сделать обертку для хука, чтоб язык менялся без перезагрузки
if (hasError) {
return (
<Center minH='100vh'>
<Center minH='100vh' data-testid='error-boundary'>
<VStack spacing={4} textAlign='center'>
<Heading as='h1' size='2xl'>
<Heading as='h1' size='2xl' data-testid='error-title'>
{i18next.t('~:dry-wash.errorBoundary.title')}
</Heading>
<Text fontSize='lg'>
<Text fontSize='lg' data-testid='error-description'>
{i18next.t('~:dry-wash.errorBoundary.description')}
</Text>
<Button
colorScheme='teal'
size='lg'
variant='outline'
data-testid='error-reload-button'
onClick={() => window.location.reload()}
>
{i18next.t('~:dry-wash.errorBoundary.button.reload')}

View File

@ -0,0 +1,51 @@
import * as React from 'react';
import { describe, expect, it, jest } from '@jest/globals';
import { render, screen, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { useEffect } from 'react';
import ErrorBoundary from '../ErrorBoundary';
import { store } from '../../../__data__/store';
const ProblematicComponent = () => {
useEffect(() => {
throw new Error('Test Error');
}, []);
return <div>Этот текст не должен появиться</div>;
};
jest.mock('@brojs/cli', () => {
return {
getNavigationValue: () => '/auth/login',
getConfigValue: () => '/api',
};
});
describe('ErrorBoundary', () => {
it('должен отобразить запасной UI при ошибке', async () => {
// Подавляем вывод ошибки в консоль во время теста
const consoleSpy = jest.spyOn(console, 'error');
consoleSpy.mockImplementation(() => {});
const { container } = render(
<Provider store={store}>
<ErrorBoundary>
<BrowserRouter>
<ProblematicComponent />
</BrowserRouter>
</ErrorBoundary>
</Provider>,
);
const button = await waitFor(() =>
screen.getByTestId('error-reload-button'),
);
expect(button).not.toBeNull();
expect(container).toMatchSnapshot();
// Восстанавливаем console.error после теста
consoleSpy.mockRestore();
});
});

View File

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorBoundary должен отобразить запасной UI при ошибке 1`] = `
<div>
<div
class="css-1o0ed15"
data-testid="error-boundary"
>
<div
class="chakra-stack css-zefqyp"
>
<h1
class="chakra-heading css-0"
data-testid="error-title"
/>
<p
class="chakra-text css-1ezsviu"
data-testid="error-description"
/>
<button
class="chakra-button css-4xx2wk"
data-testid="error-reload-button"
type="button"
/>
</div>
</div>
</div>
`;

View File

@ -15,13 +15,13 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import OrderItem from '../OrderItem';
import { OrderProps } from '../OrderItem/OrderItem';
import DateNavigator from '../DateNavigator';
import {
useGetMastersQuery,
useGetOrdersQuery,
} from '../../__data__/service/api';
import useShowToast from '../../hooks/useShowToast';
import { OrderArm } from '../../models/api';
const TABLE_HEADERS = [
'carNumber' as const,
@ -70,7 +70,6 @@ const Orders = () => {
<Heading size='lg' mb='5'>
{t('title')}
</Heading>
<DateNavigator
currentDate={currentDate}
onPreviousDate={() =>
@ -112,7 +111,7 @@ const Orders = () => {
allMasters={masters}
key={index}
{...order}
status={order.status as OrderProps['status']}
status={order.status as OrderArm['status']}
/>
))}
</Tbody>

View File

@ -5,6 +5,7 @@ export const PageSpinner: FC = () => {
return (
<Flex w='full' h='100vh' justifyContent='center' alignItems='center'>
<Spinner
data-testid='spinner'
thickness='5px'
speed='0.65s'
emptyColor='gray.200'

View File

@ -47,6 +47,7 @@ const Sidebar = () => {
w='100%'
colorScheme={isActive(URLs.armMaster.url) ? 'green' : 'blue'}
variant={isActive(URLs.armMaster.url) ? 'solid' : 'ghost'}
data-testid='master-button'
>
{t('master')}
</Button>

View File

@ -0,0 +1,102 @@
import React, { FC, memo, useRef } from 'react';
import { Controller, useForm } from 'react-hook-form';
import {
Button,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
HStack,
Input,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { landingApi } from '../../../__data__/service/landing.api';
import { UploadCarImage } from '../../../models/api';
import { useHandleUploadCarImageResponse } from './helper';
type FormValues = {
carImg: File & {
fileName: string;
};
};
type CarImageFormProps = {
orderId: UploadCarImage.Params['orderId'];
};
export const CarImageForm: FC<CarImageFormProps> = memo(function CarImageForm({
orderId,
}) {
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<FormValues>({ shouldFocusError: true });
const [uploadCarImage, uploadCarImageMutation] =
landingApi.useUploadCarImageMutation();
useHandleUploadCarImageResponse(uploadCarImageMutation);
const onSubmit = (formData: FormValues) => {
const body = new FormData();
body.append('file', formData.carImg);
uploadCarImage({ orderId, body });
};
const fileInputRef = useRef(null);
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-view.upload-car-image',
});
return (
<form>
<FormControl>
<FormLabel htmlFor='carImg'>{t('field.label')}</FormLabel>
<Controller
control={control}
name='carImg'
render={({ field: { value, onChange, ...field } }) => {
return (
<HStack gap={0}>
<Input
{...field}
ref={fileInputRef}
accept='image/png,image/jpeg'
value={value?.fileName}
onChange={(event) => {
onChange(event.target.files[0]);
handleSubmit(onSubmit)();
}}
type='file'
hidden
/>
<Input
placeholder={t('file-input.placeholder')}
value={value?.name || ''}
readOnly
borderRightRadius={0}
/>
<Button
onClick={() => {
fileInputRef.current.click();
}}
isLoading={isSubmitting || uploadCarImageMutation.isLoading}
colorScheme='primary'
paddingInline={8}
borderLeftRadius={0}
>
{t('file-input.button')}
</Button>
</HStack>
);
}}
/>
<FormErrorMessage>{errors.carImg?.message}</FormErrorMessage>
<FormHelperText>{t('field.help')}</FormHelperText>
</FormControl>
</form>
);
});

View File

@ -0,0 +1,35 @@
import { useEffect } from "react";
import { useToast } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { isErrorMessage } from "../../../models/api";
export const useHandleUploadCarImageResponse = (query: {
isSuccess: boolean;
isError: boolean;
error?: unknown;
}) => {
const toast = useToast();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-view.upload-car-image-query',
});
useEffect(() => {
if (query.isError) {
toast({
status: 'error',
title: t('error.title'),
description: isErrorMessage(query.error) ? query.error : undefined,
});
}
}, [query.isError]);
useEffect(() => {
if (query.isSuccess) {
toast({
status: 'success',
title: t('success.title'),
});
}
}, [query.isSuccess]);
};

View File

@ -0,0 +1 @@
export { CarImageForm } from './car-img-form';

View File

@ -5,11 +5,13 @@ import {
Heading,
HStack,
UnorderedList,
VStack,
ListItem,
Text,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import localizedFormat from "dayjs/plugin/localizedFormat";
dayjs.extend(localizedFormat);
import { Order } from '../../../models/landing';
import { formatDatetime } from '../../../lib';
@ -41,7 +43,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
location,
startWashTime,
endWashTime,
...props
created
}) => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-view.details',
@ -51,7 +53,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
});
return (
<VStack p={4} alignItems='flex-start' gap={4} {...props}>
<>
<HStack
width='full'
flexWrap='wrap'
@ -59,7 +61,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
gap={2}
>
<Heading as='h2' size='lg'>
{t('title', { number: orderNumber })}
{t('title', { number: orderNumber })} ({dayjs(created).format('LLLL')})
</Heading>
<OrderStatus value={status} />
</HStack>
@ -105,7 +107,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
<AlertIcon />
{t('alert')}
</Alert>
</VStack>
</>
);
};

View File

@ -9,7 +9,7 @@ export const isErrorMessage = (error: unknown): error is ErrorMessage => typeof
type ErrorResponse = {
success: false;
message: ErrorMessage;
error: ErrorMessage;
};
export type BaseResponse<Body> = SuccessResponse<Body> | ErrorResponse;

View File

@ -21,6 +21,17 @@ export namespace CreateOrder {
};
}
export namespace UploadCarImage {
export type Response = void;
export type Params = {
orderId: Order.Id;
/**
* @example { file: File }
*/
body: FormData;
};
}
type GetArrItemType<ArrType> =
ArrType extends Array<infer ItemType> ? ItemType : never;

View File

@ -0,0 +1,643 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Master Page should display master list and show details when master button is clicked 1`] = `
<div>
<div
class="css-1yeiifd"
>
<div
class="css-13owfwq"
>
<h2
class="chakra-heading css-173d1bl"
>
Сухой мастер
</h2>
<div
class="chakra-stack css-1cggwyz"
>
<hr
aria-orientation="horizontal"
class="chakra-divider css-svjswr"
/>
<a
class="chakra-button css-18yoix2"
href="/order"
>
Заказы
</a>
<hr
aria-orientation="horizontal"
class="chakra-divider css-svjswr"
/>
<a
class="chakra-button css-1kg18wp"
data-testid="master-button"
href="/master"
>
Мастера
</a>
<hr
aria-orientation="horizontal"
class="chakra-divider css-svjswr"
/>
</div>
</div>
<div
class="css-jiwy8d"
>
<div
class="css-1glkkdp"
>
<div
class="css-sd3fvu"
>
<h2
class="chakra-heading css-1jb3vzl"
>
Мастера
</h2>
<button
class="chakra-button css-h211ee"
type="button"
>
+
Добавить
</button>
</div>
<table
class="chakra-table css-5605sr"
>
<thead
class="css-0"
>
<tr
class="css-0"
>
<th
class="css-1szkfps"
>
Имя
</th>
<th
class="css-1szkfps"
>
Актуальная занятость
</th>
<th
class="css-1szkfps"
>
Телефон
</th>
<th
class="css-1szkfps"
>
Действия
</th>
</tr>
</thead>
<tbody
class="css-0"
>
<tr
class="css-0"
>
<td
class="css-zgoslk"
>
<div
class="chakra-editable css-vtl58r"
>
<div
class="chakra-stack css-c2wmld"
>
<span
class="chakra-editable__preview css-1gasyng"
>
Иван Иванов
</span>
<input
class="chakra-editable__input chakra-input css-2lpiar"
hidden=""
value="Иван Иванов"
/>
<div
class="css-1l4w6pd"
>
<button
aria-label="Edit"
class="chakra-button css-1pqvhxt"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
</div>
</div>
</div>
</td>
<td
class="css-zgoslk"
>
<p
class="chakra-text css-q9k0mw"
>
Свободен
</p>
</td>
<td
class="css-zgoslk"
>
<div
class="chakra-editable css-vtl58r"
>
<div
class="chakra-stack css-c2wmld"
>
<span
class="chakra-editable__preview css-1gasyng"
>
+7 900 123 45 67
</span>
<input
class="chakra-editable__input chakra-input css-2lpiar"
hidden=""
value="+7 900 123 45 67"
/>
<div
class="css-1l4w6pd"
>
<button
aria-label="Edit"
class="chakra-button css-1pqvhxt"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
</div>
</div>
</div>
</td>
<td
class="css-zgoslk"
>
<button
aria-controls="menu-list-:r2:"
aria-expanded="false"
aria-haspopup="menu"
class="chakra-button chakra-menu__menu-button css-13sr8jm"
id="menu-button-:r2:"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
<div
class="css-ktd6ms"
style="visibility: hidden; position: absolute; min-width: max-content; inset: 0 auto auto 0;"
>
<div
aria-orientation="vertical"
class="chakra-menu__menu-list css-s5t7bz"
id="menu-list-:r2:"
role="menu"
style="transform-origin: var(--popper-transform-origin); opacity: 0; visibility: hidden; transform: scale(0.8) translateZ(0);"
tabindex="-1"
>
<button
aria-disabled="false"
class="chakra-menu__menuitem css-y7jzs3"
data-index="0"
id="menu-list-:r2:-menuitem-:r3:"
role="menuitem"
tabindex="-1"
type="button"
>
Удалить мастера
</button>
</div>
</div>
</td>
</tr>
<tr
class="css-0"
>
<td
class="css-zgoslk"
>
<div
class="chakra-editable css-vtl58r"
>
<div
class="chakra-stack css-c2wmld"
>
<span
class="chakra-editable__preview css-1gasyng"
>
Олег Макаров
</span>
<input
class="chakra-editable__input chakra-input css-2lpiar"
hidden=""
value="Олег Макаров"
/>
<div
class="css-1l4w6pd"
>
<button
aria-label="Edit"
class="chakra-button css-1pqvhxt"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
</div>
</div>
</div>
</td>
<td
class="css-zgoslk"
>
<p
class="chakra-text css-q9k0mw"
>
Свободен
</p>
</td>
<td
class="css-zgoslk"
>
<div
class="chakra-editable css-vtl58r"
>
<div
class="chakra-stack css-c2wmld"
>
<span
class="chakra-editable__preview css-1gasyng"
>
79001234567
</span>
<input
class="chakra-editable__input chakra-input css-2lpiar"
hidden=""
value="79001234567"
/>
<div
class="css-1l4w6pd"
>
<button
aria-label="Edit"
class="chakra-button css-1pqvhxt"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
</div>
</div>
</div>
</td>
<td
class="css-zgoslk"
>
<button
aria-controls="menu-list-:r5:"
aria-expanded="false"
aria-haspopup="menu"
class="chakra-button chakra-menu__menu-button css-13sr8jm"
id="menu-button-:r5:"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
<div
class="css-ktd6ms"
style="visibility: hidden; position: absolute; min-width: max-content; inset: 0 auto auto 0;"
>
<div
aria-orientation="vertical"
class="chakra-menu__menu-list css-s5t7bz"
id="menu-list-:r5:"
role="menu"
style="transform-origin: var(--popper-transform-origin); opacity: 0; visibility: hidden; transform: scale(0.8) translateZ(0);"
tabindex="-1"
>
<button
aria-disabled="false"
class="chakra-menu__menuitem css-y7jzs3"
data-index="0"
id="menu-list-:r5:-menuitem-:r6:"
role="menuitem"
tabindex="-1"
type="button"
>
Удалить мастера
</button>
</div>
</div>
</td>
</tr>
<tr
class="css-0"
>
<td
class="css-zgoslk"
>
<div
class="chakra-editable css-vtl58r"
>
<div
class="chakra-stack css-c2wmld"
>
<span
class="chakra-editable__preview css-1gasyng"
>
Иван Галкин
</span>
<input
class="chakra-editable__input chakra-input css-2lpiar"
hidden=""
value="Иван Галкин"
/>
<div
class="css-1l4w6pd"
>
<button
aria-label="Edit"
class="chakra-button css-1pqvhxt"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
</div>
</div>
</div>
</td>
<td
class="css-zgoslk"
>
<div
class="chakra-stack css-1f0wxn3"
>
<span
class="chakra-badge css-1g1qw76"
/>
<span
class="chakra-badge css-1g1qw76"
/>
</div>
</td>
<td
class="css-zgoslk"
>
<div
class="chakra-editable css-vtl58r"
>
<div
class="chakra-stack css-c2wmld"
>
<span
class="chakra-editable__preview css-1gasyng"
>
+7 900 123 45 67
</span>
<input
class="chakra-editable__input chakra-input css-2lpiar"
hidden=""
value="+7 900 123 45 67"
/>
<div
class="css-1l4w6pd"
>
<button
aria-label="Edit"
class="chakra-button css-1pqvhxt"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
</div>
</div>
</div>
</td>
<td
class="css-zgoslk"
>
<button
aria-controls="menu-list-:r8:"
aria-expanded="false"
aria-haspopup="menu"
class="chakra-button chakra-menu__menu-button css-13sr8jm"
id="menu-button-:r8:"
type="button"
>
<svg
aria-hidden="true"
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</g>
</svg>
</button>
<div
class="css-ktd6ms"
style="visibility: hidden; position: absolute; min-width: max-content; inset: 0 auto auto 0;"
>
<div
aria-orientation="vertical"
class="chakra-menu__menu-list css-s5t7bz"
id="menu-list-:r8:"
role="menu"
style="transform-origin: var(--popper-transform-origin); opacity: 0; visibility: hidden; transform: scale(0.8) translateZ(0);"
tabindex="-1"
>
<button
aria-disabled="false"
class="chakra-menu__menuitem css-y7jzs3"
data-index="0"
id="menu-list-:r8:-menuitem-:r9:"
role="menuitem"
tabindex="-1"
type="button"
>
Удалить мастера
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<span
hidden=""
id="__chakra_env"
/>
</div>
`;

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Order View page, initial load shows order details 1`] = `
exports[`Страница просмотра заказа отображает детали заказа после успешной загрузки 1`] = `
<div>
<div
class="chakra-container css-3n6qh3"
@ -16,7 +16,6 @@ exports[`Order View page, initial load shows order details 1`] = `
</h2>
<div
class="chakra-stack css-1n38vgh"
created="2025-01-19T14:04:02.985Z"
data-testid="order-details"
>
<div
@ -26,6 +25,9 @@ exports[`Order View page, initial load shows order details 1`] = `
class="chakra-heading css-1jb3vzl"
>
Заказ №{{number}}
(
Sunday, January 19, 2025 5:04 PM
)
</h2>
<span
class="css-6jfsiv"
@ -126,7 +128,7 @@ exports[`Order View page, initial load shows order details 1`] = `
</div>
`;
exports[`Order View page, initial load shows order details loading 1`] = `
exports[`Страница просмотра заказа отображает индикатор загрузки деталей заказа 1`] = `
<div>
<div
class="chakra-container css-3n6qh3"
@ -171,7 +173,7 @@ exports[`Order View page, initial load shows order details loading 1`] = `
</div>
`;
exports[`Order View page, initial load shows order error 1`] = `
exports[`Страница просмотра заказа отображает ошибку при некорректном ID заказа 1`] = `
<div>
<div
class="chakra-container css-3n6qh3"

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Arm Page render 1`] = `
exports[`Страница заказов должна корректно отображать список заказов после загрузки данных 1`] = `
<div>
<div
class="css-1yeiifd"
@ -32,6 +32,7 @@ exports[`Arm Page render 1`] = `
/>
<a
class="chakra-button css-1kg18wp"
data-testid="master-button"
href="/auth/login"
>
Мастера
@ -74,173 +75,7 @@ exports[`Arm Page render 1`] = `
<p
class="chakra-text css-52ukzg"
>
15.02.2025
</p>
<button
class="chakra-button css-ez23ye"
type="button"
>
<svg
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
fill="currentColor"
/>
</svg>
</button>
</div>
<table
class="chakra-table css-5605sr"
>
<thead
class="css-0"
>
<tr
class="css-0"
>
<th
class="css-1szkfps"
>
Номер машины
</th>
<th
class="css-1szkfps"
>
Дата заказа
</th>
<th
class="css-1szkfps"
>
Статус
</th>
<th
class="css-1szkfps"
>
Мастер
</th>
<th
class="css-1szkfps"
>
Телефон
</th>
<th
class="css-1szkfps"
>
Расположение
</th>
</tr>
</thead>
<tbody
class="css-0"
>
<tr
class="css-0"
>
<td
class="css-1v9gmks"
colspan="6"
>
<div
class="chakra-spinner css-1j92705"
>
<span
class="css-8b45rq"
>
Loading...
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<span
hidden=""
id="__chakra_env"
/>
</div>
`;
exports[`Arm Page render 2`] = `
<div>
<div
class="css-1yeiifd"
>
<div
class="css-13owfwq"
>
<h2
class="chakra-heading css-173d1bl"
>
Сухой мастер
</h2>
<div
class="chakra-stack css-1cggwyz"
>
<hr
aria-orientation="horizontal"
class="chakra-divider css-svjswr"
/>
<a
class="chakra-button css-1kg18wp"
href="/auth/login"
>
Заказы
</a>
<hr
aria-orientation="horizontal"
class="chakra-divider css-svjswr"
/>
<a
class="chakra-button css-1kg18wp"
href="/auth/login"
>
Мастера
</a>
<hr
aria-orientation="horizontal"
class="chakra-divider css-svjswr"
/>
</div>
</div>
<div
class="css-jiwy8d"
>
<div
class="css-1glkkdp"
>
<h2
class="chakra-heading css-1xer3cv"
>
Заказы
</h2>
<div
class="css-1u3smh"
>
<button
class="chakra-button css-ez23ye"
type="button"
>
<svg
class="chakra-icon css-onkibi"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
fill="currentColor"
/>
</svg>
</button>
<p
class="chakra-text css-52ukzg"
>
15.02.2025
23.02.2025
</p>
<button
class="chakra-button css-ez23ye"

View File

@ -0,0 +1,133 @@
import * as React from 'react';
import {
describe,
it,
expect,
jest,
beforeAll,
afterEach,
afterAll,
} from '@jest/globals';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import ErrorBoundary from '../../components/ErrorBoundary';
import { store } from '../../__data__/store';
import Page from '../arm';
const server = setupServer(
http.get('/api/arm/masters', () => {
return HttpResponse.json({
success: true,
body: [
{
id: '4545423234',
name: 'Иван Иванов',
phone: '+7 900 123 45 67',
},
{
name: 'Олег Макаров',
phone: '79001234567',
id: '23423442',
},
{
id: '345354234',
name: 'Иван Галкин',
schedule: [
{
id: 'order1',
startWashTime: '2024-11-24T10:30:00.000Z',
endWashTime: '2024-11-24T16:30:00.000Z',
},
{
id: 'order2',
startWashTime: '2024-11-24T11:30:00.000Z',
endWashTime: '2024-11-24T17:30:00.000Z',
},
],
phone: '+7 900 123 45 67',
},
],
});
}),
http.post('/api/arm/orders', () => {
return HttpResponse.json({
success: true,
body: [
{
id: 'order1',
carNumber: 'A123BC',
startWashTime: '2024-11-24T10:30:00.000Z',
endWashTime: '2024-11-24T16:30:00.000Z',
orderDate: '2024-11-24T08:41:46.366Z',
status: 'pending',
phone: '79001234563',
location: 'Казань, ул. Баумана, 1',
master: {
name: 'Олег Макаров',
phone: '79001234567',
id: '23423442',
},
notes: '',
},
{
id: 'order2',
carNumber: 'A245BC',
startWashTime: '2024-11-24T11:30:00.000Z',
endWashTime: '2024-11-24T17:30:00.000Z',
orderDate: '2024-11-24T07:40:46.366Z',
status: 'progress',
phone: '79001234567',
location: 'Казань, ул. Баумана, 43',
master: [],
notes: '',
},
],
});
}),
);
jest.mock('@brojs/cli', () => {
return {
getNavigationValue: (key: string) =>
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('../../../bro.config').navigations[key],
getConfigValue: () => '/api',
};
});
describe('Master Page', () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('should display master list and show details when master button is clicked', async () => {
const { container } = render(
<Provider store={store}>
<ChakraProvider theme={chakraTheme}>
<ErrorBoundary>
<BrowserRouter>
<Page mockUser={{ name: 'ilnaz' }} />
</BrowserRouter>
</ErrorBoundary>
</ChakraProvider>
</Provider>,
);
const button = await waitFor(() => screen.getByTestId('master-button'));
fireEvent.click(button);
// Проверяем отображение всех мастеров
await waitFor(() => {
expect(screen.getByText('Иван Иванов')).toBeInTheDocument();
expect(screen.getByText('Олег Макаров')).toBeInTheDocument();
expect(screen.getByText('Иван Галкин')).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
});

View File

@ -11,12 +11,12 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
}));
describe('Order View page, initial load', () => {
describe('Страница просмотра заказа', () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('shows order details loading', () => {
test('отображает индикатор загрузки деталей заказа', () => {
(useParams as jest.Mock).mockReturnValue({ orderId: 'id1' });
const { container } = render(
@ -33,7 +33,7 @@ describe('Order View page, initial load', () => {
expect(container).toMatchSnapshot();
});
test('shows order details', async () => {
test('отображает детали заказа после успешной загрузки', async () => {
(useParams as jest.Mock).mockReturnValue({ orderId: 'id1' });
const { container } = render(
@ -52,7 +52,7 @@ describe('Order View page, initial load', () => {
expect(container).toMatchSnapshot();
});
test('shows order error', async () => {
test('отображает ошибку при некорректном ID заказа', async () => {
(useParams as jest.Mock).mockReturnValue({ orderId: null });
const { container } = render(

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import {
describe,
it,
jest,
beforeAll,
afterEach,
afterAll,
} from '@jest/globals';
import { render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import ErrorBoundary from '../../components/ErrorBoundary';
import { store } from '../../__data__/store';
import Page from '../arm';
const server = setupServer(
http.post('/api/arm/orders', () => {
return HttpResponse.json({
success: true,
body: [],
});
}),
http.get('/api/arm/masters', () => {
return HttpResponse.json({
success: true,
body: [],
});
}),
);
jest.mock('@brojs/cli', () => {
return {
getNavigationValue: () => '/auth/login',
getConfigValue: () => '/api',
};
});
describe('order page', () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('получение пустого списка', async () => {
render(
<Provider store={store}>
<ChakraProvider theme={chakraTheme}>
<ErrorBoundary>
<BrowserRouter>
<Page mockUser={{ name: 'ilnaz' }} />
</BrowserRouter>
</ErrorBoundary>
</ChakraProvider>
</Provider>,
);
await waitFor(() => screen.getByText('Список пуст'));
});
});

View File

@ -0,0 +1,98 @@
import * as React from 'react';
import {
describe,
it,
jest,
beforeAll,
afterEach,
afterAll,
expect,
} from '@jest/globals';
import { render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import ErrorBoundary from '../../components/ErrorBoundary';
import { store } from '../../__data__/store';
import Page from '../arm';
import { PageSpinner } from '../../components';
const server = setupServer(
http.post('/api/arm/orders', () => {
return HttpResponse.json({}, { status: 500 });
}),
http.get('/api/arm/masters', () => {
return HttpResponse.json({
success: true,
body: [
{
id: '4545423234',
name: 'Иван Иванов',
phone: '+7 900 123 45 67',
},
{
name: 'Олег Макаров',
phone: '79001234567',
id: '23423442',
},
{
id: '345354234',
name: 'Иван Галкин',
schedule: [
{
id: 'order1',
startWashTime: '2024-11-24T10:30:00.000Z',
endWashTime: '2024-11-24T16:30:00.000Z',
},
{
id: 'order2',
startWashTime: '2024-11-24T11:30:00.000Z',
endWashTime: '2024-11-24T17:30:00.000Z',
},
],
phone: '+7 900 123 45 67',
},
],
});
}),
);
jest.mock('@brojs/cli', () => {
return {
getNavigationValue: () => '/auth/login',
getConfigValue: () => '/api',
};
});
describe('order page', () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('обработка ошибки при загрузке данных', async () => {
render(
<Provider store={store}>
<ChakraProvider theme={chakraTheme}>
<ErrorBoundary>
<BrowserRouter>
<Page mockUser={{ name: 'ilnaz' }} />
</BrowserRouter>
</ErrorBoundary>
</ChakraProvider>
</Provider>,
);
await waitFor(() => screen.getByText('Ошибка при загрузке данных'));
});
});
describe('Routers', () => {
it('отображает PageSpinner ', async () => {
render(<PageSpinner />);
expect(await screen.findByTestId('spinner')).toBeTruthy();
});
});

View File

@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import {
describe,
it,
@ -98,16 +98,12 @@ jest.mock('@brojs/cli', () => {
};
});
describe('Arm Page', () => {
describe('Страница заказов', () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('render ', async () => {
server.events.on('request:start', ({ request }) => {
console.log('Outgoing:', request.method, request.url);
});
it('должна корректно отображать список заказов после загрузки данных', async () => {
const { container } = render(
<Provider store={store}>
<ChakraProvider theme={chakraTheme}>
@ -120,8 +116,6 @@ describe('Arm Page', () => {
</Provider>,
);
expect(container).toMatchSnapshot();
await waitFor(() => screen.getByText('A123BC'));
expect(container).toMatchSnapshot();

View File

@ -21,6 +21,7 @@ import { Order } from '../../models/landing';
import { landingApi } from '../../__data__/service/landing.api';
import { isErrorMessage } from '../../models/api';
import { FEATURE } from '../../__data__/features';
import { CarImageForm } from '../../components/order-view/car-img';
const Page: FC = () => {
const { t } = useTranslation('~', {
@ -69,24 +70,30 @@ const Page: FC = () => {
<>
<>
{isSuccess && (
<OrderDetails
orderNumber={order.orderNumber}
status={order.status}
phone={order.phone}
carNumber={order.carNumber}
carBody={order.carBody}
carColor={order.carColor}
location={order.location}
startWashTime={order.startWashTime}
endWashTime={order.endWashTime}
created={order.created}
data-testid='order-details'
/>
<VStack p={4} alignItems='flex-start' gap={4} data-testid='order-details'>
<OrderDetails
orderNumber={order.orderNumber}
status={order.status}
phone={order.phone}
carNumber={order.carNumber}
carBody={order.carBody}
carColor={order.carColor}
location={order.location}
startWashTime={order.startWashTime}
endWashTime={order.endWashTime}
created={order.created}
/>
{FEATURE.carImageUpload.isOn && <CarImageForm orderId={orderId} />}
</VStack>
)}
</>
<>
{isError && (
<Alert status='error' alignItems='flex-start' data-testid='error'>
<Alert
status='error'
alignItems='flex-start'
data-testid='error'
>
<AlertIcon />
<Box>
<AlertTitle>

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const router = require('express').Router();
const STUBS = { masters: 'success', orders: 'success', orderCreate: 'success', orderView: 'success-pending' };
const STUBS = { masters: 'success', orders: 'success', orderCreate: 'success', orderView: 'success-pending', orderCarImg: 'success' };
router.get('/set/:name/:value', (req, res) => {
const { name, value } = req.params;
@ -15,28 +15,34 @@ router.get('/set/:name/:value', (req, res) => {
router.get('/', (req, res) => {
res.send(`<div>
<fieldset>
<legend>Мастера</legend>
<legend>Мастера</legend>
${generateRadioInput('masters', 'success')}
${generateRadioInput('masters', 'error')}
${generateRadioInput('masters', 'empty')}
</fieldset>
<fieldset>
<legend>Заказы</legend>
<legend>Заказы</legend>
${generateRadioInput('orders', 'success')}
${generateRadioInput('orders', 'error')}
${generateRadioInput('orders', 'empty')}
</fieldset>
<fieldset>
<legend>Лендинг - Сделать заказ</legend>
<legend>Лендинг - Сделать заказ</legend>
${generateRadioInput('orderCreate', 'success')}
${generateRadioInput('orderCreate', 'error')}
</fieldset>
<fieldset>
<legend>Лендинг - Детали заказа</legend>
<legend>Лендинг - Детали заказа</legend>
${generateRadioInput('orderView', 'success-pending')}
${generateRadioInput('orderView', 'success-working')}
${generateRadioInput('orderView', 'error')}
</fieldset>
<fieldset>
<legend>Лендинг - Детали заказа, фото машины</legend>
${generateRadioInput('orderCarImg', 'success')}
${generateRadioInput('orderCarImg', 'error-file-type')}
${generateRadioInput('orderCarImg', 'error-file-size')}
</fieldset>
</div>`);
});

View File

@ -98,6 +98,22 @@ router.post('/order/create', (req, res) => {
);
});
router.post('/order/:orderId/upload-car-img', (req, res) => {
const { orderId } = req.params;
const stubName = `${orderId}-${STUBS.orderCarImg}`;
try {
res
.status(/error/.test(stubName) ? 500 : 200)
.send(require(`../json/landing-order-car-image-upload/${stubName}.json`));
} catch (e) {
console.error(e);
res
.status(500)
.send(commonError);
}
});
router.use('/admin', require('./admin'));
module.exports = router;

View File

@ -0,0 +1,4 @@
{
"success": false,
"error": "Invalid car image file size. Limit is 5MB"
}

View File

@ -0,0 +1,4 @@
{
"success": false,
"error": "Invalid car image file type. Allowed types: jpg, png"
}

View File

@ -0,0 +1,3 @@
{
"success": true
}

View File

@ -1,4 +1,4 @@
{
"success": false,
"message": "Не удалось создать заказ"
"error": "Не удалось создать заказ"
}

View File

@ -1,4 +1,4 @@
{
"success": false,
"message": "Не удалось загрузить детали заказа"
"error": "Не удалось загрузить детали заказа"
}