From 858b86235c5dc173975f4aea53df38107f1157d8 Mon Sep 17 00:00:00 2001 From: Egor Berezovskiy Date: Tue, 7 Jan 2025 11:40:55 +0100 Subject: [PATCH 1/2] front: stdcm consist form validation Signed-off-by: Egor Berezovskiy --- front/public/locales/en/stdcm.json | 16 +++- front/public/locales/fr/stdcm.json | 16 +++- .../components/StdcmForm/StdcmConfig.tsx | 44 ++++++++++- .../components/StdcmForm/StdcmConsist.tsx | 48 +++++++++++- front/src/applications/stdcm/consts.ts | 14 +--- front/src/applications/stdcm/types.ts | 7 ++ .../stdcm/utils/consistValidation.ts | 78 +++++++++++++++++++ .../styles/scss/applications/stdcm/_card.scss | 13 +++- 8 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 front/src/applications/stdcm/utils/consistValidation.ts diff --git a/front/public/locales/en/stdcm.json b/front/public/locales/en/stdcm.json index 31172b49e02..c2b36708ed4 100644 --- a/front/public/locales/en/stdcm.json +++ b/front/public/locales/en/stdcm.json @@ -8,7 +8,21 @@ "tonnage": "Tonnage", "maxSpeed": "Max speed", "tractionEngine": "Traction engine", - "towedRollingStock": "Towed rolling stock" + "towedRollingStock": "Towed rolling stock", + "errors": { + "totalMass": { + "negative": "Must be positive.", + "range": "The total weight must be between {{low}} and {{high}}t" + }, + "totalLength": { + "negative": "Must be positive.", + "range": "The total length must be between {{low}} and {{high}}m" + }, + "maxSpeed": { + "negative": "Must be positive.", + "range": "The max speed must be between {{low}} and {{high}}km/h" + } + } }, "datetimeOutsideWindow": "Date must be between {{low}} and {{high}}", "debug": { diff --git a/front/public/locales/fr/stdcm.json b/front/public/locales/fr/stdcm.json index 30a085dd736..4a42fd5125e 100644 --- a/front/public/locales/fr/stdcm.json +++ b/front/public/locales/fr/stdcm.json @@ -8,7 +8,21 @@ "tonnage": "Tonnage", "maxSpeed": "Vitesse max.", "tractionEngine": "Engin de traction", - "towedRollingStock": "Matériel remorqué" + "towedRollingStock": "Matériel remorqué", + "errors": { + "totalMass": { + "negative": "Doit être positif.", + "range": "Le tonnage total doit être compris entre {{low}} et {{high}}t" + }, + "totalLength": { + "negative": "Doit être positif.", + "range": "La longueur totale doit être comprise entre {{low}} et {{high}}m" + }, + "maxSpeed": { + "negative": "Doit être positif.", + "range": "La vitesse max. doit être comprise entre {{low}} et {{high}}km/h" + } + } }, "datetimeOutsideWindow": "La date et l'heure doivent être comprises entre le {{low}} et le {{high}}", "debug": { diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx index b330b391b03..f607811db87 100644 --- a/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx +++ b/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx @@ -5,9 +5,16 @@ import cx from 'classnames'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import useStdcmTowedRollingStock from 'applications/stdcm/hooks/useStdcmTowedRollingStock'; import { extractMarkersInfo } from 'applications/stdcm/utils'; +import { + validateMaxSpeed, + validateTotalLength, + validateTotalMass, +} from 'applications/stdcm/utils/consistValidation'; import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; import useInfraStatus from 'modules/pathfinding/hooks/useInfraStatus'; +import { useStoreDataForRollingStockSelector } from 'modules/rollingStock/components/RollingStockSelector/useStoreDataForRollingStockSelector'; import { Map } from 'modules/trainschedule/components/ManageTrainSchedule'; import { type StdcmConfSliceActions, resetMargins } from 'reducers/osrdconf/stdcmConf'; import type { StdcmConfSelectors } from 'reducers/osrdconf/stdcmConf/selectors'; @@ -61,6 +68,9 @@ const StdcmConfig = ({ getProjectID, getScenarioID, getStudyID, + getTotalMass, + getTotalLength, + getMaxSpeed, } = useOsrdConfSelectors() as StdcmConfSelectors; const origin = useSelector(getStdcmOrigin); const pathSteps = useSelector(getStdcmPathSteps); @@ -69,11 +79,38 @@ const StdcmConfig = ({ const studyID = useSelector(getStudyID); const scenarioID = useSelector(getScenarioID); + const totalMass = useSelector(getTotalMass); + const totalLength = useSelector(getTotalLength); + const maxSpeed = useSelector(getMaxSpeed); + const pathfinding = useStaticPathfinding(infra); const formRef = useRef(null); const [formErrors, setFormErrors] = useState(); + const { rollingStock } = useStoreDataForRollingStockSelector(); + const towedRollingStock = useStdcmTowedRollingStock(); + + const consistErrors = useMemo(() => { + const totalMassError = validateTotalMass({ + tractionEngineMass: rollingStock?.mass, + towedMass: towedRollingStock?.mass, + totalMass, + }); + + const totalLengthError = validateTotalLength({ + tractionEngineLength: rollingStock?.length, + towedLength: towedRollingStock?.length, + totalLength, + }); + + return { + totalMass: totalMassError, + totalLength: totalLengthError, + maxSpeed: validateMaxSpeed(maxSpeed, rollingStock?.max_speed), + }; + }, [rollingStock, towedRollingStock, totalMass, totalLength, maxSpeed]); + const disabled = isPending || retainedSimulationIndex > -1; const markersInfo = useMemo(() => extractMarkersInfo(pathSteps), [pathSteps]); @@ -146,7 +183,7 @@ const StdcmConfig = ({ />
- +
@@ -175,7 +212,10 @@ const StdcmConfig = ({ isDisabled={ disabled || !showBtnToLaunchSimulation || - formErrors?.errorType === StdcmConfigErrorTypes.INFRA_NOT_LOADED + formErrors?.errorType === StdcmConfigErrorTypes.INFRA_NOT_LOADED || + !!consistErrors.totalMass || + !!consistErrors.totalLength || + !!consistErrors.maxSpeed } /> {formErrors && ( diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx index ec647c58123..7f0000e3732 100644 --- a/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx +++ b/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx @@ -4,6 +4,11 @@ import { Input, ComboBox } from '@osrd-project/ui-core'; import { useTranslation } from 'react-i18next'; import useStdcmTowedRollingStock from 'applications/stdcm/hooks/useStdcmTowedRollingStock'; +import { + CONSIST_MAX_SPEED_MIN, + CONSIST_TOTAL_LENGTH_MAX, + CONSIST_TOTAL_MASS_MAX, +} from 'applications/stdcm/utils/consistValidation'; import type { LightRollingStockWithLiveries, TowedRollingStock } from 'common/api/osrdEditoastApi'; import { useOsrdConfActions } from 'common/osrdContext'; import SpeedLimitByTagSelector from 'common/SpeedLimitByTagSelector/SpeedLimitByTagSelector'; @@ -14,6 +19,7 @@ import useFilterRollingStock from 'modules/rollingStock/hooks/useFilterRollingSt import useFilterTowedRollingStock from 'modules/towedRollingStock/hooks/useFilterTowedRollingStock'; import { type StdcmConfSliceActions } from 'reducers/osrdconf/stdcmConf'; import { useAppDispatch } from 'store'; +import { kgToT, kmhToMs, msToKmh } from 'utils/physics'; import StdcmCard from './StdcmCard'; import useStdcmConsist from '../../hooks/useStdcmConsist'; @@ -33,7 +39,7 @@ const ConsistCardTitle = ({ ); }; -const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { +const StdcmConsist = ({ consistErrors = {}, disabled = false }: StdcmConfigCardProps) => { const { t } = useTranslation('stdcm'); const { speedLimitByTag, speedLimitsByTags, dispatchUpdateSpeedLimitByTag } = useStoreDataForSpeedLimitByTagSelector({ isStdcm: true }); @@ -166,6 +172,20 @@ const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { value={totalMass ?? ''} onChange={onTotalMassChange} disabled={disabled} + statusWithMessage={ + consistErrors?.totalMass + ? { + status: 'error', + tooltip: 'left', + message: t(consistErrors.totalMass, { + low: Math.floor( + kgToT((rollingStock?.mass ?? 0) + (towedRollingStock?.mass ?? 0)) + ), + high: CONSIST_TOTAL_MASS_MAX, + }), + } + : undefined + } /> { value={totalLength ?? ''} onChange={onTotalLengthChange} disabled={disabled} + statusWithMessage={ + consistErrors?.totalLength + ? { + status: 'error', + tooltip: 'right', + message: t(consistErrors.totalLength, { + low: Math.floor((rollingStock?.length ?? 0) + (towedRollingStock?.length ?? 0)), + high: CONSIST_TOTAL_LENGTH_MAX, + }), + } + : undefined + } />
@@ -194,6 +226,20 @@ const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { value={maxSpeed ?? ''} onChange={onMaxSpeedChange} disabled={disabled} + statusWithMessage={ + consistErrors?.maxSpeed + ? { + status: 'error', + tooltip: 'right', + message: t(consistErrors.maxSpeed, { + low: CONSIST_MAX_SPEED_MIN, + high: Math.floor( + msToKmh(Math.min(rollingStock?.max_speed ?? kmhToMs(CONSIST_MAX_SPEED_MIN))) + ), + }), + } + : undefined + } />
diff --git a/front/src/applications/stdcm/consts.ts b/front/src/applications/stdcm/consts.ts index 76f50bdfa08..a1bd3625a42 100644 --- a/front/src/applications/stdcm/consts.ts +++ b/front/src/applications/stdcm/consts.ts @@ -9,18 +9,6 @@ export const STDCM_REQUEST_STATUS = Object.freeze({ export const STDCM_TRAIN_ID = -10; -export const COMPOSITION_CODES = [ - 'HLP', - 'MA100', - 'MA80', - 'MA90', - 'ME100', - 'ME120', - 'ME140', - 'MV160', - 'MVGV', -]; - export const COMPOSITION_CODES_MAX_SPEEDS: Record = { MA80: 80, MA90: 90, @@ -32,3 +20,5 @@ export const COMPOSITION_CODES_MAX_SPEEDS: Record = HLP: 100, MVGV: 200, }; + +export const COMPOSITION_CODES = Object.keys(COMPOSITION_CODES_MAX_SPEEDS); diff --git a/front/src/applications/stdcm/types.ts b/front/src/applications/stdcm/types.ts index e7cee4b13fc..66dd2c4d3bf 100644 --- a/front/src/applications/stdcm/types.ts +++ b/front/src/applications/stdcm/types.ts @@ -71,6 +71,12 @@ export type StdcmResultsOperationalPoint = { trackName?: string; }; +export type ConsistErrors = { + totalMass?: string; + totalLength?: string; + maxSpeed?: string; +}; + export type StdcmResults = { stdcmResponse: StdcmSuccessResponse; speedSpaceChartData: SpeedSpaceChartData | null; @@ -145,6 +151,7 @@ export type StdcmSimulation = { /** This type is used for StdcmConsist, StdcmOrigin, StdcmDestination and StdcmVias components */ export type StdcmConfigCardProps = { disabled?: boolean; + consistErrors?: ConsistErrors; }; export enum ArrivalTimeTypes { diff --git a/front/src/applications/stdcm/utils/consistValidation.ts b/front/src/applications/stdcm/utils/consistValidation.ts new file mode 100644 index 00000000000..08f94aef904 --- /dev/null +++ b/front/src/applications/stdcm/utils/consistValidation.ts @@ -0,0 +1,78 @@ +import { kgToT, msToKmh } from 'utils/physics'; + +export const CONSIST_TOTAL_MASS_MAX = 10000; // ton +export const CONSIST_TOTAL_LENGTH_MAX = 750; // m +export const CONSIST_MAX_SPEED_MIN = 30; // km/h + +export const validateTotalMass = ({ + tractionEngineMass = 0, + towedMass = 0, + totalMass, +}: { + tractionEngineMass?: number; + towedMass?: number; + totalMass?: number; +}) => { + if (!totalMass) { + return undefined; + } + + if (totalMass <= 0) { + return 'consist.errors.totalMass.negative'; + } + + const tractionMassInTons = kgToT(tractionEngineMass); + const consistMassInTons = kgToT(tractionEngineMass + towedMass); + const massLimit = towedMass ? consistMassInTons : tractionMassInTons; + + if (totalMass < massLimit || totalMass >= CONSIST_TOTAL_MASS_MAX) { + return 'consist.errors.totalMass.range'; + } + + return undefined; +}; + +export const validateTotalLength = ({ + tractionEngineLength = 0, + towedLength = 0, + totalLength, +}: { + tractionEngineLength?: number; + towedLength?: number; + totalLength?: number; +}) => { + if (!totalLength) { + return undefined; + } + + if (totalLength <= 0) { + return 'consist.errors.totalLength.negative'; + } + + const consistLength = Math.floor(tractionEngineLength + towedLength); + + if (totalLength < consistLength || totalLength >= CONSIST_TOTAL_LENGTH_MAX) { + return 'consist.errors.totalLength.range'; + } + + return undefined; +}; + +export const validateMaxSpeed = (maxSpeed?: number, tractionEngineMaxSpeed?: number) => { + if (!maxSpeed) { + return undefined; + } + + if (maxSpeed <= 0) { + return 'consist.errors.maxSpeed.negative'; + } + + if ( + maxSpeed < CONSIST_MAX_SPEED_MIN || + (tractionEngineMaxSpeed && maxSpeed > Math.floor(msToKmh(tractionEngineMaxSpeed))) + ) { + return 'consist.errors.maxSpeed.range'; + } + + return undefined; +}; diff --git a/front/src/styles/scss/applications/stdcm/_card.scss b/front/src/styles/scss/applications/stdcm/_card.scss index fb25345aa26..9b48b12f5cd 100644 --- a/front/src/styles/scss/applications/stdcm/_card.scss +++ b/front/src/styles/scss/applications/stdcm/_card.scss @@ -7,6 +7,9 @@ background-color: rgba(255, 255, 255, 1); height: fit-content; position: relative; + display: flex; + flex-direction: column; + gap: 5px; .stdcm-default-card-icon { color: #1844ef; @@ -89,7 +92,9 @@ } .feed-back { - padding: 0; + --field-wrapper-padding-bottom: 9px; + padding-top: 3px; + padding-bottom: 3px; } .date-picker { @@ -121,7 +126,8 @@ // cards styles &.consist { - padding-left: 24px; + padding-left: 11px; + padding-right: 11px; padding-bottom: 15px; .towed-rolling-stock { @@ -130,9 +136,8 @@ .stdcm-consist__properties { display: grid; - grid-template-columns: 119px 127px; + grid-template-columns: 139px 147px; justify-content: space-between; - padding-block: 8px; .osrd-config-item { padding-right: 3px; From 5fb08e470dedb873c4c684343c82f12055de6c39 Mon Sep 17 00:00:00 2001 From: maymanaf Date: Wed, 8 Jan 2025 14:03:43 +0100 Subject: [PATCH 2/2] front: fix stdcm e2e tests Signed-off-by: maymanaf --- front/tests/006-stdcm.spec.ts | 6 ++-- front/tests/assets/stdcm/stdcmAllStops.json | 14 ++++---- front/tests/assets/stdcm/stdcmWithAllVia.json | 8 ++--- .../assets/stdcm/stdcmWithoutAllVia.json | 6 ++-- front/tests/pages/stdcm-page-model.ts | 34 ++++++++++--------- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/front/tests/006-stdcm.spec.ts b/front/tests/006-stdcm.spec.ts index bb5846652e8..ae669ed1494 100644 --- a/front/tests/006-stdcm.spec.ts +++ b/front/tests/006-stdcm.spec.ts @@ -20,11 +20,11 @@ test.describe('Verify train schedule elements and filters', () => { let infra: Infra; let OSRDLanguage: string; let createdTowedRollingStock: TowedRollingStock; - const UPDATED_TONNAGE = '561'; + const UPDATED_TONNAGE = '1061'; const consistDetails: ConsistFields = { tractionEngine: electricRollingStockName, - tonnage: '400', - length: '300', + tonnage: '950', + length: '567', maxSpeed: '180', speedLimitTag: 'HLP', }; diff --git a/front/tests/assets/stdcm/stdcmAllStops.json b/front/tests/assets/stdcm/stdcmAllStops.json index c757f486cbd..f391b2cf0c6 100644 --- a/front/tests/assets/stdcm/stdcmAllStops.json +++ b/front/tests/assets/stdcm/stdcmAllStops.json @@ -6,7 +6,7 @@ "endStop": "", "passageStop": "", "startStop": "00:55", - "weight": "400t", + "weight": "950t", "refEngine": "" }, { @@ -23,9 +23,9 @@ "index": 3, "operationalPoint": "Mid_East_station", "code": "BV", - "endStop": "01:21", + "endStop": "01:22", "passageStop": "3 min", - "startStop": "01:24", + "startStop": "01:25", "weight": "=", "refEngine": "=" }, @@ -33,9 +33,9 @@ "index": 4, "operationalPoint": "North_station", "code": "BV", - "endStop": "01:32", + "endStop": "01:33", "passageStop": "4 min", - "startStop": "01:36", + "startStop": "01:37", "weight": "=", "refEngine": "=" }, @@ -43,10 +43,10 @@ "index": 5, "operationalPoint": "South_station", "code": "BV", - "endStop": "01:39", + "endStop": "01:41", "passageStop": "", "startStop": "", - "weight": "400t", + "weight": "950t", "refEngine": "" } ] diff --git a/front/tests/assets/stdcm/stdcmWithAllVia.json b/front/tests/assets/stdcm/stdcmWithAllVia.json index 58fc36edc89..b736cbf0119 100644 --- a/front/tests/assets/stdcm/stdcmWithAllVia.json +++ b/front/tests/assets/stdcm/stdcmWithAllVia.json @@ -6,7 +6,7 @@ "endStop": "", "passageStop": "", "startStop": "20:21", - "weight": "400t", + "weight": "950t", "refEngine": "" }, { @@ -34,7 +34,7 @@ "operationalPoint": "North_station", "code": "BV", "endStop": "", - "passageStop": "20:52", + "passageStop": "20:53", "startStop": "", "weight": "=", "refEngine": "=" @@ -43,10 +43,10 @@ "index": 5, "operationalPoint": "South_station", "code": "BV", - "endStop": "20:56", + "endStop": "20:57", "passageStop": "", "startStop": "", - "weight": "400t", + "weight": "950t", "refEngine": "" } ] diff --git a/front/tests/assets/stdcm/stdcmWithoutAllVia.json b/front/tests/assets/stdcm/stdcmWithoutAllVia.json index 4ecd64c702b..db0a316ae77 100644 --- a/front/tests/assets/stdcm/stdcmWithoutAllVia.json +++ b/front/tests/assets/stdcm/stdcmWithoutAllVia.json @@ -6,7 +6,7 @@ "endStop": "", "passageStop": "", "startStop": "20:21", - "weight": "400t", + "weight": "950t", "refEngine": "" }, { @@ -23,10 +23,10 @@ "index": 3, "operationalPoint": "South_station", "code": "BV", - "endStop": "20:56", + "endStop": "20:57", "passageStop": "", "startStop": "", - "weight": "400t", + "weight": "950t", "refEngine": "" } ] diff --git a/front/tests/pages/stdcm-page-model.ts b/front/tests/pages/stdcm-page-model.ts index b29c317def7..a6a12bcdd52 100644 --- a/front/tests/pages/stdcm-page-model.ts +++ b/front/tests/pages/stdcm-page-model.ts @@ -35,7 +35,7 @@ export interface ConsistFields { speedLimitTag?: string; } const EXPECT_TO_PASS_TIMEOUT = 90_000; // Since toPass ignores custom expect timeouts, this timeout is set to account for all actions within the function. - +const MINIMUM_SIMULATION_NUMBER = 1; class STDCMPage { readonly page: Page; @@ -606,21 +606,23 @@ class STDCMPage { } // Launch the simulation and check if simulation-related elements are visible - async launchSimulation() { - await expect(async () => { - await this.launchSimulationButton.waitFor(); - await expect(this.launchSimulationButton).toBeEnabled(); - await this.launchSimulationButton.click({ force: true }); - const simulationElements = await this.simulationList.all(); - await Promise.all(simulationElements.map((simulationElement) => simulationElement.waitFor())); - expect(await this.simulationList.count()).toBeGreaterThanOrEqual(1); - // Check map result container visibility only for Chromium browser - if (this.page.context().browser()?.browserType().name() === 'chromium') { - await expect(this.mapResultContainer).toBeVisible(); - } - }).toPass({ - timeout: EXPECT_TO_PASS_TIMEOUT, - }); + async launchSimulation(): Promise { + await this.launchSimulationButton.waitFor({ state: 'visible' }); + await expect(this.launchSimulationButton).toBeEnabled(); + await this.launchSimulationButton.click({ force: true }); + // Wait for simulation elements to load and validate their presence + await this.simulationList.waitFor({ timeout: 60_000 }); + const simulationElements = await this.simulationList.all(); + + if (simulationElements.length < MINIMUM_SIMULATION_NUMBER) { + throw new Error( + `Expected at least ${MINIMUM_SIMULATION_NUMBER} simulation, but found ${simulationElements.length}.` + ); + } + // Check map result container visibility only for Chromium browser + if (this.page.context().browser()?.browserType().name() === 'chromium') { + await expect(this.mapResultContainer).toBeVisible(); + } } async verifyTableData(tableDataPath: string): Promise {