Загрузка данных
diff --git a/.gitignore b/.gitignore
index aedbf73fca709713e1920f151c064d7c5c89d5c2..81032cd0664cf01d0646f2decaf1f8b0627f6d1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ storybook-static
.env.local
.yalc
yalc.lock
+coverage/
+.scannerwork/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 980c1aaa172272f1e5a281ddf62069c127054e67..d6f3b77b635eb8ee1a6c42a74f3a352157a66b87 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,6 +24,7 @@ include:
stages:
- build
+ - test
- sast
- publish
@@ -43,7 +44,7 @@ build:
- |
npm config set loglevel verbose
npm config set ignore-scripts true
- rm package-lock.json
+ rm -f package-lock.json
VERSION=${CI_COMMIT_TAG:-"0.0.0"}
npm version $VERSION --no-git-tag-version || true
@@ -62,6 +63,26 @@ build:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+test:
+ stage: test
+ image: moex-docker-static-hosted-nexus-dev.tech.moex.com/node:22.11.0-bookworm-jq
+ script:
+ - |
+ npm config set loglevel verbose
+ npm config set ignore-scripts true
+
+ npm install
+ npm run coverage
+
+ test -f coverage/lcov.info
+ artifacts:
+ paths:
+ - coverage/lcov.info
+ expire_in: 1 day
+ rules:
+ - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+
publish:
stage: publish
image: moex-docker-static-hosted-nexus-dev.tech.moex.com/node:22.11.0-bookworm-jq
@@ -73,11 +94,12 @@ publish:
- |
cd pack-result
TGZ_FILE=$(ls -t ${PACKAGE_NAME}-*.tgz 2>/dev/null | head -n 1)
- curl -v -H "Authorization: Basic ${DOP_NPM_NEXUS_TOKEN}" \
+ curl -v \
+ -H "Authorization: Basic ${DOP_NPM_NEXUS_TOKEN}" \
-X POST \
"https://nexus-dev.tech.moex.com/service/rest/v1/components?repository=trade-radar-npm-private-hosted" \
-F "npm.asset=@./${TGZ_FILE};type=application/octet-stream" \
--fail
rules:
- if: $CI_COMMIT_TAG
- when: manual
\ No newline at end of file
+ when: manual
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa8198b8d229faf97c90cba0bddebae3550de1ee..74a642bc597b9a430024fe54eb400511305f21b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# latest
+- Исправлен баг, при котором элементы графика перекрывали шапку сайта moex
+
+# 0.1.22
+
- Добавлен индикатор RSI
- Исправлен баг, когда линия EMA уходила в будущее
- Исправлен баг, при котором при завершении построения траектории добавлялась лишняя точка
diff --git a/jest.config.ts b/jest.config.ts
index d4450712a110219fa4c470b968e83ee0b221f078..f1af148e43fe95b67da6c9f33b97db47adda3a8c 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -7,9 +7,8 @@ import { pathsToModuleNameMapper } from 'ts-jest';
import type { Config } from '@jest/types';
-
-const fileContent = readFileSync('./tsconfig.json')
-const fileContentJSON = JSON5.parse(fileContent.toString())
+const fileContent = readFileSync('./tsconfig.json');
+const fileContentJSON = JSON5.parse(fileContent.toString());
const config: Config.InitialOptions = {
// нужен пакет, который поддержит резолв scss модуля
@@ -28,7 +27,9 @@ const config: Config.InitialOptions = {
moduleNameMapper: {
...pathsToModuleNameMapper(fileContentJSON.compilerOptions.paths),
'\\.(css|scss|module.scss)$': '<rootDir>/__mocks__/styleMock.js',
- }
-}
+ },
+ coverageDirectory: '<rootDir>/coverage',
+ coverageReporters: ['text', 'lcov'],
+};
-export default config
+export default config;
diff --git a/package.json b/package.json
index 7c47d5910430ef20cb536ab71acd875fa52ca8de..d13259a4ae9eb954f81a5fde9abe618aba4ee91e 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"build:lib:types": "tsc --project tsconfig.types.json && tsc-alias -p tsconfig.types.json",
"build:lib": "npm run clean:dist && npm run clean:types && npm run build:lib:js && npm run build:lib:types",
"test": "jest",
+ "test:related": "jest --findRelatedTests --passWithNoTests",
"coverage": "jest --coverage",
"lint:fix": "npx prettier src --write && npx eslint src --fix",
"lint": "npx eslint src",
@@ -157,7 +158,11 @@
"@storybook/mdx2-csf": "1.0.0"
},
"lint-staged": {
- "*.{js,jsx,ts,tsx,css,scss,md,html}": [
+ "*.{ts,tsx}": [
+ "npm run lint:fix",
+ "npm run test:related --"
+ ],
+ "*.{js,jsx,css,scss,md,html}": [
"npm run lint:fix"
]
}
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000000000000000000000000000000000000..e9343cdd6bb64b22a153a9cd0475adcaaf004bab
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,6 @@
+sonar.sources=src
+sonar.tests=src
+sonar.test.inclusions=**/*.test.ts,**/*.test.tsx
+sonar.coverage.exclusions=**/*.test.ts,**/*.test.tsx,**/*.d.ts,**/*.stories.ts,**/*.stories.tsx,jest.config.ts
+sonar.cpd.exclusions=**/*.test.ts,**/*.test.tsx,**/*.mock.ts,**/*.mock.tsx,**/*.stories.ts,**/*.stories.tsx
+sonar.javascript.lcov.reportPaths=coverage/lcov.info
diff --git a/src/components/Dropdown/index.module.scss b/src/components/Dropdown/index.module.scss
index b4b6fac69fd62e0a5a9b3db1979d3258a1431816..b531f637de8ab438fc3b4f407330276c882ef4d6 100644
--- a/src/components/Dropdown/index.module.scss
+++ b/src/components/Dropdown/index.module.scss
@@ -11,6 +11,8 @@ $menu-width: 200px;
}
.menu {
+ @include m.scrollbar;
+
position: fixed;
box-sizing: border-box;
width: var(--dropdown-width, $menu-width);
diff --git a/src/core/ContainerManager.ts b/src/core/ContainerManager.ts
index b0be74ef0b2922a670b2b0bb44f2519ef16a5c21..0e1f93b2d15ba4c04a98961ff36a39e8badb23f0 100644
--- a/src/core/ContainerManager.ts
+++ b/src/core/ContainerManager.ts
@@ -15,9 +15,10 @@ interface CreateContainersOptions {
enum ZIndex {
Chart = '0',
- Base = '10',
- Floating = '20',
- Modal = '1000',
+ Base = '1',
+ Floating = '2',
+ Legend = '3',
+ Modal = '100',
}
const ContainerLayoutConfig = {
@@ -176,7 +177,7 @@ export class ContainerManager {
legendContainer.style.position = 'absolute';
legendContainer.style.top = '0';
legendContainer.style.left = '0';
- legendContainer.style.zIndex = ZIndex.Base;
+ legendContainer.style.zIndex = ZIndex.Legend;
legendContainer.style.pointerEvents = 'none';
const paneOverlayContainer = document.createElement('div');
diff --git a/src/core/__tests__/ContainerManager.test.ts b/src/core/__tests__/ContainerManager.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ca0b7f77839ba03aca54d4eebc9294f6c457b4d5
--- /dev/null
+++ b/src/core/__tests__/ContainerManager.test.ts
@@ -0,0 +1,200 @@
+import {
+ CHART_DRAWING_TOOLBAR_CONTAINER,
+ CHART_MODAL_CONTAINER_CLASSNAME,
+ CHART_PANE_OVERLAY_CONTAINER,
+ CHART_ROOT_CLASSNAME,
+} from '@src/constants';
+
+import { ContainerManager } from '../ContainerManager';
+
+jest.mock('@src/constants', () => ({
+ CHART_DRAWING_TOOLBAR_CONTAINER: 'chart-drawing-toolbar-container',
+ CHART_MODAL_CONTAINER_CLASSNAME: 'chart-modal-container',
+ CHART_PANE_OVERLAY_CONTAINER: 'chart-pane-overlay-container',
+ CHART_ROOT_CLASSNAME: 'chart-root',
+}));
+
+describe('ContainerManager', () => {
+ let parentContainer: HTMLDivElement;
+
+ beforeEach(() => {
+ document.querySelectorAll('link[href*="fonts.googleapis.com"]').forEach((link) => link.remove());
+
+ parentContainer = document.createElement('div');
+ document.body.appendChild(parentContainer);
+ });
+
+ afterEach(() => {
+ parentContainer.remove();
+ });
+
+ describe('createContainers', () => {
+ it('должен создавать основные контейнеры графика с корректной структурой', () => {
+ const {
+ headerContainer,
+ footerContainer,
+ chartContainer,
+ chartAreaContainer,
+ toolBarContainer,
+ modalContainer,
+ controlBarContainer,
+ drawingToolbarContainer,
+ } = ContainerManager.createContainers({
+ parentContainer,
+ showBottomPanel: true,
+ showMenuButton: true,
+ });
+
+ expect(parentContainer.classList.contains(CHART_ROOT_CLASSNAME)).toBe(true);
+
+ expect(parentContainer.children[0]).toBe(headerContainer);
+ expect(parentContainer.children[1]).toBe(chartContainer);
+ expect(parentContainer.children[2]).toBe(footerContainer);
+
+ expect(chartContainer.contains(toolBarContainer)).toBe(true);
+ expect(chartContainer.contains(chartAreaContainer)).toBe(true);
+ expect(chartContainer.contains(modalContainer)).toBe(true);
+
+ expect(chartAreaContainer.contains(controlBarContainer)).toBe(true);
+ expect(chartAreaContainer.contains(drawingToolbarContainer)).toBe(true);
+
+ expect(drawingToolbarContainer.classList.contains(CHART_DRAWING_TOOLBAR_CONTAINER)).toBe(true);
+
+ expect(modalContainer.classList.contains(CHART_MODAL_CONTAINER_CLASSNAME)).toBe(true);
+
+ expect(modalContainer.hidden).toBe(true);
+ });
+
+ it('должен создавать сетку с нижней панелью', () => {
+ ContainerManager.createContainers({
+ parentContainer,
+ showBottomPanel: true,
+ showMenuButton: true,
+ });
+
+ expect(parentContainer.style.gridTemplateRows).toBe('36px minmax(0, 1fr) 32px');
+ });
+
+ it('должен создавать сетку без нижней панели', () => {
+ const { footerContainer } = ContainerManager.createContainers({
+ parentContainer,
+ showBottomPanel: false,
+ showMenuButton: true,
+ });
+
+ expect(parentContainer.style.gridTemplateRows).toBe('36px minmax(0, 1fr)');
+ expect(parentContainer.contains(footerContainer)).toBe(false);
+ });
+
+ it('должен отображать панель инструментов при включённой кнопке меню', () => {
+ const { chartContainer, toolBarContainer } = ContainerManager.createContainers({
+ parentContainer,
+ showMenuButton: true,
+ });
+
+ expect(chartContainer.contains(toolBarContainer)).toBe(true);
+ expect(chartContainer.style.gridTemplateColumns).toBe('42px minmax(0, 1fr)');
+ });
+
+ it('должен скрывать панель инструментов при выключенной кнопке меню', () => {
+ const { chartContainer, toolBarContainer } = ContainerManager.createContainers({
+ parentContainer,
+ showMenuButton: false,
+ });
+
+ expect(chartContainer.contains(toolBarContainer)).toBe(false);
+ expect(chartContainer.style.gridTemplateColumns).toBe('minmax(0, 1fr)');
+ });
+
+ it('должен переключать видимость панели инструментов', () => {
+ const { chartContainer, toolBarContainer, toggleToolbar } = ContainerManager.createContainers({
+ parentContainer,
+ showMenuButton: true,
+ });
+
+ expect(toggleToolbar()).toBe(false);
+ expect(chartContainer.contains(toolBarContainer)).toBe(false);
+ expect(chartContainer.style.gridTemplateColumns).toBe('minmax(0, 1fr)');
+
+ expect(toggleToolbar()).toBe(true);
+ expect(chartContainer.contains(toolBarContainer)).toBe(true);
+ expect(chartContainer.style.gridTemplateColumns).toBe('42px minmax(0, 1fr)');
+ });
+
+ it('должен удалять старое содержимое родительского контейнера перед созданием графика', () => {
+ const oldElement = document.createElement('div');
+
+ parentContainer.appendChild(oldElement);
+
+ ContainerManager.createContainers({
+ parentContainer,
+ showMenuButton: true,
+ });
+
+ expect(parentContainer.contains(oldElement)).toBe(false);
+ });
+
+ it('должен добавлять шрифт только один раз', () => {
+ ContainerManager.createContainers({
+ parentContainer,
+ showMenuButton: true,
+ });
+
+ ContainerManager.createContainers({
+ parentContainer,
+ showMenuButton: true,
+ });
+
+ const fontLinks = document.querySelectorAll('link[href*="fonts.googleapis.com"]');
+
+ expect(fontLinks).toHaveLength(1);
+ expect(fontLinks[0]?.getAttribute('href')).toBe(
+ 'https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap',
+ );
+ });
+ });
+
+ describe('createPaneContainers', () => {
+ it('должен создавать контейнеры легенды и оверлея панели', () => {
+ const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
+
+ expect(legendContainer.style.position).toBe('absolute');
+ expect(legendContainer.style.pointerEvents).toBe('none');
+
+ expect(paneOverlayContainer.classList.contains(CHART_PANE_OVERLAY_CONTAINER)).toBe(true);
+
+ expect(paneOverlayContainer.style.position).toBe('absolute');
+ expect(paneOverlayContainer.style.pointerEvents).toBe('none');
+ });
+
+ it('должен размещать слои графика в корректном порядке', () => {
+ const { controlBarContainer, drawingToolbarContainer, modalContainer } = ContainerManager.createContainers({
+ parentContainer,
+ showMenuButton: true,
+ });
+
+ const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
+
+ const controlBarZIndex = Number(controlBarContainer.style.zIndex);
+ const paneOverlayZIndex = Number(paneOverlayContainer.style.zIndex);
+ const drawingZIndex = Number(drawingToolbarContainer.style.zIndex);
+ const legendZIndex = Number(legendContainer.style.zIndex);
+ const modalZIndex = Number(modalContainer.style.zIndex);
+
+ expect(controlBarZIndex).toBe(paneOverlayZIndex);
+ expect(drawingZIndex).toBeGreaterThan(paneOverlayZIndex);
+ expect(legendZIndex).toBeGreaterThan(drawingZIndex);
+ expect(modalZIndex).toBeGreaterThan(legendZIndex);
+ });
+ });
+
+ describe('clearContainers', () => {
+ it('должен очищать содержимое родительского контейнера', () => {
+ parentContainer.innerHTML = '<div>content</div>';
+
+ ContainerManager.clearContainers(parentContainer);
+
+ expect(parentContainer.innerHTML).toBe('');
+ });
+ });
+});
diff --git a/src/core/styles.module.scss b/src/core/styles.module.scss
index 7de23753e704e4942e61710017a3be10494b84f1..685cbe25d7b51cc64a0c94cc11893cdca7ae91f1 100644
--- a/src/core/styles.module.scss
+++ b/src/core/styles.module.scss
@@ -1,32 +1,7 @@
-.scrollableBox {
- &::-webkit-scrollbar {
- height: 4px;
- }
-
- &::-webkit-scrollbar-button {
- display: none;
- }
-
- &::-webkit-scrollbar-track {
- background: #00000000;
- border-radius: 4px;
- }
-
- &::-webkit-scrollbar-thumb {
- background: #4a5568;
- border-radius: 4px;
- }
+@use '../theme/mixins' as m;
- &::-webkit-scrollbar-thumb:hover {
- background: var(--neutral-6);
- }
-}
-
-@supports not selector(::-webkit-scrollbar) {
- .scrollableBox {
- scrollbar-width: thin;
- scrollbar-color: var(--neutral-6) #00000000;
- }
+.scrollableBox {
+ @include m.scrollbar;
}
.safariFullscreen {
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 1ff16163940d4d90ded40ec0aea010cb7fbcd206..ac4bd31a770cfd0c8cee0c490fbe2e9afa8fb0ae 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -2,13 +2,15 @@
.moex-chart-root {
font-family: 'Inter', sans-serif;
+ position: relative;
+ z-index: 0;
}
.moex-chart-portal-host {
position: fixed;
inset: 0;
pointer-events: none;
- z-index: 1100;
+ z-index: 110;
}
.moex-chart-portal-host > * {
@@ -22,7 +24,7 @@
.moex-price-scale-controls {
position: absolute;
inset: 0;
- z-index: 10;
+ z-index: 2;
display: flex;
align-items: flex-end;
justify-content: center;
diff --git a/src/theme/_mixins.scss b/src/theme/_mixins.scss
index 7c8a06cdc891f76d1b412e4bffe69ad92657fb79..e4df9451917fae86688298c2a45ce5ef7021b392 100644
--- a/src/theme/_mixins.scss
+++ b/src/theme/_mixins.scss
@@ -90,3 +90,40 @@
border-right-color: var(--tooltip-bg) !important;
}
}
+
+@mixin scrollbar {
+ @supports selector(::-webkit-scrollbar) {
+ &::-webkit-scrollbar {
+ width: var(--space-0250);
+ height: var(--space-0250);
+ }
+
+ &::-webkit-scrollbar-button {
+ display: none;
+ width: 0;
+ height: 0;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--neutral-6);
+ border-radius: var(--space-2000);
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: var(--neutral-6);
+ }
+
+ &::-webkit-scrollbar-corner {
+ background: transparent;
+ }
+ }
+
+ @supports not selector(::-webkit-scrollbar) {
+ scrollbar-width: thin;
+ scrollbar-color: var(--neutral-6) transparent;
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index b33f3d4b95b9c1a839bf1d5af27338f7089b2051..27c78cf1810ec0e064002d9a61b8c82781735ddd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -28,8 +28,8 @@
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
- "sourceMap": true,
+ "sourceMap": true
},
- "include": ["src", "stories", ".storybook"],
+ "include": ["src", "stories", ".storybook", "jest.config.ts"],
"exclude": ["dist", "node_modules"]
}