Forráskód Böngészése

Error message on on_change when creating Scenarios in scenario_selector (#2067)

* Error message on on_change when creating Scenarios with scenario_selector
resolves #2009

* starting jest env to test the core comps

* run gui-core jest tests

* do not install taipy-gui

* no need to build

* test script

* build gui to test core

* passing tests

* fix missing classes

* update gui mock

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 7 hónapja
szülő
commit
55bdd46584

+ 88 - 3
.github/workflows/frontend.yml

@@ -36,7 +36,7 @@ jobs:
       - uses: actions/setup-python@v5
       - uses: actions/setup-python@v5
         with:
         with:
           python-version: "3.11"
           python-version: "3.11"
-      - name: npm build and test with node ${{ matrix.node-version }} on ${{ matrix.os }}
+      - name: npm test with node ${{ matrix.node-version }} on ${{ matrix.os }}
         uses: actions/setup-node@v4
         uses: actions/setup-node@v4
         with:
         with:
           node-version: ${{ matrix.node-version }}
           node-version: ${{ matrix.node-version }}
@@ -65,8 +65,6 @@ jobs:
       - name: Install dependencies
       - name: Install dependencies
         if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
         if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
         run: npm ci
         run: npm ci
-      - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
-        run: npm run build --if-present
 
 
       - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
       - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
         run: npm test
         run: npm test
@@ -82,3 +80,90 @@ jobs:
           annotations: failed-tests
           annotations: failed-tests
           # use if you want to avoid errors on the base branch coverage (ie no coverage and no comparison but as it fails anyway as it uses npm install)
           # use if you want to avoid errors on the base branch coverage (ie no coverage and no comparison but as it fails anyway as it uses npm install)
           # base-coverage-file: ./report.json
           # base-coverage-file: ./report.json
+
+  frontend-core-jest:
+    timeout-minutes: 20
+    strategy:
+      matrix:
+        node-version: [20.x]
+        os: [ubuntu-latest, windows-latest, macos-13]
+    runs-on: ${{ matrix.os }}
+
+    defaults:
+      run:
+        working-directory: ./frontend/taipy
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.11"
+      - name: npm build and test with node ${{ matrix.node-version }} on ${{ matrix.os }}
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: "npm"
+          cache-dependency-path: "**/package-lock.json"
+
+      - name: Hash taipy gui source code
+        id: hash-gui-fe
+        working-directory: ./
+        run: |
+          python tools/frontend/hash_source.py --taipy-gui-only
+          echo "HASH=$(cat hash.txt)" >> $GITHUB_OUTPUT
+          rm hash.txt
+        shell: bash
+      - name: Restore cached frontend build
+        id: cache-gui-fe-build
+        uses: actions/cache@v4
+        with:
+          path: taipy/gui/webapp
+          key: taipy-gui-frontend-build-${{ runner.os }}-${{ steps.hash-gui-fe.outputs.HASH }}
+
+      - name: Hash taipy gui core source code
+        id: hash-gui-core-fe
+        working-directory: ./
+        run: |
+          python tools/frontend/hash_source.py --taipy-gui-core-only
+          echo "HASH=$(cat hash.txt)" >> $GITHUB_OUTPUT
+          rm hash.txt
+        shell: bash
+      - name: Restore cached core frontend build
+        id: cache-gui-core-fe-build
+        uses: actions/cache@v4
+        with:
+          path: taipy/gui_core/lib
+          key: taipy-gui-core-frontend-build-${{ runner.os }}-${{ steps.hash-gui-core-fe.outputs.HASH }}
+
+      - name: Taipy-gui Install dom dependencies
+        if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        working-directory: ./frontend/taipy-gui/dom
+        run: npm ci
+      - name: Install Taipy-gui dependencies
+        if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        working-directory: ./frontend/taipy-gui
+        run: npm ci
+      - name: Build Taipy-gui
+        if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        working-directory: ./frontend/taipy-gui
+        run: npm run build
+
+      - name: Install dependencies
+        if: steps.cache-gui-core-fe-build.outputs.cache-hit != 'true'
+        run: npm ci
+
+      - name: Test Taipy
+        if: steps.cache-gui-core-fe-build.outputs.cache-hit != 'true'
+        run: npm test
+
+      - name: Code coverage
+        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-core-fe-build.outputs.cache-hit != 'true'
+        uses: artiomtr/jest-coverage-report-action@v2
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          threshold: "80"
+          working-directory: "./frontend/taipy"
+          skip-step: install
+          annotations: failed-tests
+          # use if you want to avoid errors on the base branch coverage (ie no coverage and no comparison but as it fails anyway as it uses npm install)
+          # base-coverage-file: ./report.json

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 203 - 344
frontend/taipy-gui/package-lock.json


+ 19 - 0
frontend/taipy-gui/packaging/taipy-gui.d.ts

@@ -22,6 +22,15 @@ import * as React from "react";
  */
  */
 export declare const getUpdateVar: (updateVars: string, name: string) => string | undefined;
 export declare const getUpdateVar: (updateVars: string, name: string) => string | undefined;
 
 
+/**
+ * Appends a suffix to the class names.
+ *
+ * @param names - The class names.
+ * @param suffix - The suffix to append.
+ * @returns The new list of class names.
+ */
+export declare const getSuffixedClassNames: (names: string | undefined, suffix: string) => string;
+
 export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
 export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
     defaultActive?: boolean;
     defaultActive?: boolean;
     active?: boolean;
     active?: boolean;
@@ -403,6 +412,16 @@ export declare const Context: React.Context<Store>;
  * @returns The latest updated value.
  * @returns The latest updated value.
  */
  */
 export declare const useDynamicProperty: <T>(value: T, defaultValue: T, defaultStatic: T) => T;
 export declare const useDynamicProperty: <T>(value: T, defaultValue: T, defaultStatic: T) => T;
+/**
+ * A React hook to manage classNames (dynamic and static).
+ * cf. useDynamicProperty
+ *
+ * @param libClassName - The default static className.
+ * @param dynamicClassName - The bound className.
+ * @param className - The default user set className.
+ * @returns The complete list of applicable classNames.
+ */
+export declare const useClassNames: (libClassName?: string, dynamicClassName?: string, className?: string) => string;
 /**
 /**
  * A React hook to manage a dynamic json property.
  * A React hook to manage a dynamic json property.
  *
  *

+ 2 - 2
frontend/taipy-gui/src/components/Taipy/TableSort.tsx

@@ -207,7 +207,7 @@ const TableSort = (props: TableSortProps) => {
             });
             });
         },
         },
         [onValidate]
         [onValidate]
-    );
+        );
 
 
     useEffect(() => {
     useEffect(() => {
         columns &&
         columns &&
@@ -235,7 +235,7 @@ const TableSort = (props: TableSortProps) => {
                 anchorOrigin={anchorOrigin}
                 anchorOrigin={anchorOrigin}
                 open={showSort}
                 open={showSort}
                 onClose={onShowSortClick}
                 onClose={onShowSortClick}
-                className={getSuffixedClassNames(className, "-filter")}
+                className={getSuffixedClassNames(className, "-sort")}
             >
             >
                 <Grid container sx={gridSx} gap={0.5}>
                 <Grid container sx={gridSx} gap={0.5}>
                     {sorts.map((sd, idx) => (
                     {sorts.map((sd, idx) => (

+ 7 - 0
frontend/taipy-gui/src/components/Taipy/utils.ts

@@ -125,6 +125,13 @@ export const getCssSize = (val: string | number) => {
     return val;
     return val;
 };
 };
 
 
+/**
+ * Appends a suffix to the class names.
+ *
+ * @param names - The class names.
+ * @param suffix - The suffix to append.
+ * @returns The new list of class names.
+ */
 export const getSuffixedClassNames = (names: string | undefined, suffix: string) =>
 export const getSuffixedClassNames = (names: string | undefined, suffix: string) =>
     (names || "")
     (names || "")
         .split(/\s+/)
         .split(/\s+/)

+ 2 - 1
frontend/taipy-gui/src/extensions/exports.ts

@@ -24,7 +24,7 @@ import {getComponentClassName} from "../components/Taipy/TaipyStyle";
 import Metric from "../components/Taipy/Metric";
 import Metric from "../components/Taipy/Metric";
 import { useLovListMemo, LoV, LoVElt } from "../components/Taipy/lovUtils";
 import { useLovListMemo, LoV, LoVElt } from "../components/Taipy/lovUtils";
 import { LovItem } from "../utils/lov";
 import { LovItem } from "../utils/lov";
-import { getUpdateVar } from "../components/Taipy/utils";
+import { getUpdateVar, getSuffixedClassNames } from "../components/Taipy/utils";
 import { ColumnDesc, RowType, RowValue } from "../components/Taipy/tableUtils";
 import { ColumnDesc, RowType, RowValue } from "../components/Taipy/tableUtils";
 import { TaipyContext, TaipyStore } from "../context/taipyContext";
 import { TaipyContext, TaipyStore } from "../context/taipyContext";
 import { TaipyBaseAction, TaipyState } from "../context/taipyReducers";
 import { TaipyBaseAction, TaipyState } from "../context/taipyReducers";
@@ -59,6 +59,7 @@ export {
     createSendActionNameAction,
     createSendActionNameAction,
     createSendUpdateAction,
     createSendUpdateAction,
     getComponentClassName,
     getComponentClassName,
+    getSuffixedClassNames,
     getUpdateVar,
     getUpdateVar,
     useClassNames,
     useClassNames,
     useDispatchRequestUpdateOnFirstRender,
     useDispatchRequestUpdateOnFirstRender,

+ 32 - 0
frontend/taipy/jest.config.js

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+const { createJsWithTsPreset } = require('ts-jest')
+
+
+/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
+module.exports = {
+    testEnvironment: "jsdom",
+    setupFilesAfterEnv: [
+        "./test-config/jest.env.js",
+        "./test-config/createObjectUrl.js",
+        "./test-config/Canvas.js",
+        "./test-config/intersectionObserver.js",
+        "./test-config/nanoid.js",
+        "./test-config/guiMock.js"
+    ],
+    coverageReporters: ["json", "html", "text"],
+    modulePathIgnorePatterns: ["<rootDir>/packaging/"],
+    moduleNameMapper: {},
+    ...createJsWithTsPreset(),
+};

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 5113 - 1002
frontend/taipy/package-lock.json


+ 11 - 2
frontend/taipy/package.json

@@ -3,16 +3,24 @@
   "version": "4.0.0",
   "version": "4.0.0",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
+    "@testing-library/jest-dom": "^6.5.0",
+    "@testing-library/react": "^16.0.1",
+    "@testing-library/user-event": "^14.5.2",
+    "@types/jest": "^29.5.13",
     "@types/react": "^18.0.15",
     "@types/react": "^18.0.15",
     "@typescript-eslint/eslint-plugin": "^8.5.0",
     "@typescript-eslint/eslint-plugin": "^8.5.0",
     "@typescript-eslint/parser": "^8.5.0",
     "@typescript-eslint/parser": "^8.5.0",
     "child_process": "^1.0.2",
     "child_process": "^1.0.2",
-    "dotenv": "^16.0.3",
+    "cross-env": "^7.0.3",
+    "dotenv": "^16.4.5",
     "eslint": "^8.20.0",
     "eslint": "^8.20.0",
     "eslint-plugin-react": "^7.30.1",
     "eslint-plugin-react": "^7.30.1",
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-tsdoc": "^0.3.0",
     "eslint-plugin-tsdoc": "^0.3.0",
     "eslint-webpack-plugin": "^4.0.0",
     "eslint-webpack-plugin": "^4.0.0",
+    "jest": "^29.7.0",
+    "jest-environment-jsdom": "^29.7.0",
+    "ts-jest": "^29.2.5",
     "ts-loader": "^9.3.1",
     "ts-loader": "^9.3.1",
     "typescript": "^5.0.2",
     "typescript": "^5.0.2",
     "webpack": "^5.74.0",
     "webpack": "^5.74.0",
@@ -35,6 +43,7 @@
   "scripts": {
   "scripts": {
     "postinstall": "node scripts/install.js",
     "postinstall": "node scripts/install.js",
     "build:dev": "webpack --mode development",
     "build:dev": "webpack --mode development",
-    "build": "webpack --mode production"
+    "build": "webpack --mode production",
+    "test": "cross-env TZ=UTC jest"
   }
   }
 }
 }

+ 8 - 2
frontend/taipy/src/CoreSelector.tsx

@@ -49,6 +49,8 @@ import {
     TableFilter,
     TableFilter,
     SortDesc,
     SortDesc,
     TableSort,
     TableSort,
+    useClassNames,
+    getSuffixedClassNames,
 } from "taipy-gui";
 } from "taipy-gui";
 
 
 import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence, Sequences } from "./utils/types";
 import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence, Sequences } from "./utils/types";
@@ -343,6 +345,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
     const [expandedItems, setExpandedItems] = useState<string[]>([]);
     const [expandedItems, setExpandedItems] = useState<string[]>([]);
 
 
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
+    const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
 
 
@@ -627,17 +630,18 @@ const CoreSelector = (props: CoreSelectorProps) => {
                             appliedFilters={filters}
                             appliedFilters={filters}
                             filteredCount={0}
                             filteredCount={0}
                             onValidate={applyFilters}
                             onValidate={applyFilters}
+                            className={className}
                         ></TableFilter>
                         ></TableFilter>
                     </Grid>
                     </Grid>
                 ) : null}
                 ) : null}
                 {active && colSorts ? (
                 {active && colSorts ? (
                     <Grid>
                     <Grid>
-                        <TableSort columns={colSorts} appliedSorts={sorts} onValidate={applySorts}></TableSort>
+                        <TableSort columns={colSorts} appliedSorts={sorts} onValidate={applySorts} className={className}></TableSort>
                     </Grid>
                     </Grid>
                 ) : null}
                 ) : null}
                 {showSearch ? (
                 {showSearch ? (
                     <Grid>
                     <Grid>
-                        <IconButton onClick={onRevealSearch} size="small" sx={iconInRowSx}>
+                        <IconButton onClick={onRevealSearch} size="small" sx={iconInRowSx} className={getSuffixedClassNames(className, "-search")}>
                             {revealSearch ? (
                             {revealSearch ? (
                                 <SearchOffOutlined fontSize="inherit" />
                                 <SearchOffOutlined fontSize="inherit" />
                             ) : (
                             ) : (
@@ -659,6 +663,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
                             }
                             }
                             label="Pinned only"
                             label="Pinned only"
                             sx={labelInRowSx}
                             sx={labelInRowSx}
+                            className={getSuffixedClassNames(className, "-pins")}
                         />
                         />
                     </Grid>
                     </Grid>
                 ) : null}
                 ) : null}
@@ -670,6 +675,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
                             onChange={onSearch}
                             onChange={onSearch}
                             fullWidth
                             fullWidth
                             label="Search"
                             label="Search"
+                            className={getSuffixedClassNames(className, "-search-input")}
                         ></TextField>
                         ></TextField>
                     </Grid>
                     </Grid>
                 ) : null}
                 ) : null}

+ 1 - 1
frontend/taipy/src/DataNodeViewer.tsx

@@ -65,6 +65,7 @@ import {
     createRequestUpdateAction,
     createRequestUpdateAction,
     createSendActionNameAction,
     createSendActionNameAction,
     getUpdateVar,
     getUpdateVar,
+    useClassNames,
     useDynamicProperty,
     useDynamicProperty,
     useModule,
     useModule,
     Store,
     Store,
@@ -86,7 +87,6 @@ import {
     iconLabelSx,
     iconLabelSx,
     popoverOrigin,
     popoverOrigin,
     tinySelPinIconButtonSx,
     tinySelPinIconButtonSx,
-    useClassNames,
 } from "./utils";
 } from "./utils";
 import PropertiesEditor, { DatanodeProperties } from "./PropertiesEditor";
 import PropertiesEditor, { DatanodeProperties } from "./PropertiesEditor";
 import { NodeType, Scenarios } from "./utils/types";
 import { NodeType, Scenarios } from "./utils/types";

+ 1 - 1
frontend/taipy/src/JobSelector.tsx

@@ -57,6 +57,7 @@ import {
     createSendUpdateAction,
     createSendUpdateAction,
     getComponentClassName,
     getComponentClassName,
     getUpdateVar,
     getUpdateVar,
+    useClassNames,
     useDispatch,
     useDispatch,
     useDispatchRequestUpdateOnFirstRender,
     useDispatchRequestUpdateOnFirstRender,
     useModule,
     useModule,
@@ -66,7 +67,6 @@ import {
     disableColor,
     disableColor,
     getUpdateVarNames,
     getUpdateVarNames,
     popoverOrigin,
     popoverOrigin,
-    useClassNames,
     EllipsisSx,
     EllipsisSx,
     SecondaryEllipsisProps,
     SecondaryEllipsisProps,
     CoreProps,
     CoreProps,

+ 2 - 1
frontend/taipy/src/JobViewer.tsx

@@ -23,12 +23,13 @@ import {
     createRequestUpdateAction,
     createRequestUpdateAction,
     createSendActionNameAction,
     createSendActionNameAction,
     getUpdateVar,
     getUpdateVar,
+    useClassNames,
     useDispatch,
     useDispatch,
     useDispatchRequestUpdateOnFirstRender,
     useDispatchRequestUpdateOnFirstRender,
     useModule,
     useModule,
 } from "taipy-gui";
 } from "taipy-gui";
 
 
-import { useClassNames, EllipsisSx, SecondaryEllipsisProps, CoreProps } from "./utils";
+import { EllipsisSx, SecondaryEllipsisProps, CoreProps } from "./utils";
 import StatusChip from "./StatusChip";
 import StatusChip from "./StatusChip";
 
 
 interface JobViewerProps extends CoreProps {
 interface JobViewerProps extends CoreProps {

+ 2 - 2
frontend/taipy/src/NodeSelector.tsx

@@ -14,10 +14,10 @@
 import React from "react";
 import React from "react";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 
 
-import { CoreProps, MainTreeBoxSx, useClassNames } from "./utils";
+import { CoreProps, MainTreeBoxSx } from "./utils";
 import { Cycles, DataNodes, NodeType, Scenarios } from "./utils/types";
 import { Cycles, DataNodes, NodeType, Scenarios } from "./utils/types";
 import CoreSelector from "./CoreSelector";
 import CoreSelector from "./CoreSelector";
-import { getComponentClassName } from "taipy-gui";
+import { getComponentClassName, useClassNames } from "taipy-gui";
 
 
 interface NodeSelectorProps extends CoreProps {
 interface NodeSelectorProps extends CoreProps {
     innerDatanodes?: Cycles | Scenarios | DataNodes;
     innerDatanodes?: Cycles | Scenarios | DataNodes;

+ 2 - 1
frontend/taipy/src/ScenarioDag.tsx

@@ -25,11 +25,12 @@ import {
     createSendUpdateAction,
     createSendUpdateAction,
     getComponentClassName,
     getComponentClassName,
     getUpdateVar,
     getUpdateVar,
+    useClassNames,
     useDispatch,
     useDispatch,
     useDynamicProperty,
     useDynamicProperty,
     useModule,
     useModule,
 } from "taipy-gui";
 } from "taipy-gui";
-import { CoreProps, useClassNames } from "./utils";
+import { CoreProps } from "./utils";
 import { TaipyDiagramModel } from "./projectstorm/models";
 import { TaipyDiagramModel } from "./projectstorm/models";
 
 
 interface ScenarioDagProps extends CoreProps {
 interface ScenarioDagProps extends CoreProps {

+ 116 - 0
frontend/taipy/src/ScenarioSelector.spec.tsx

@@ -0,0 +1,116 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import React from "react";
+import { render, waitFor } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import { createContext } from "react";
+
+import ScenarioSelector from "./ScenarioSelector";
+import { useDispatchRequestUpdateOnFirstRender } from "taipy-gui";
+
+const TaipyContext = createContext<{}>({ state: {}, dispatch: () => null });
+
+describe("ScenarioSelector Component", () => {
+    it("renders", async () => {
+        const { getByText } = render(
+            <ScenarioSelector
+                onScenarioCrud="onScenarioCrud"
+                onScenarioSelect="onScenarioSelect"
+                height="50vh"
+                updateVars=""
+            />
+        );
+        const elt = getByText("Add scenario");
+        expect(elt.tagName).toBe("BUTTON");
+    });
+    it("displays the right info for string", async () => {
+        const { getByText } = render(
+            <ScenarioSelector
+                onScenarioCrud="onScenarioCrud"
+                onScenarioSelect="onScenarioSelect"
+                height="50vh"
+                updateVars=""
+                className="test"
+            />
+        );
+        const elt = getByText("Add scenario");
+        expect(elt.closest(".MuiBox-root")).toHaveClass("test");
+    });
+    it("is disabled", async () => {
+        const { getByText } = render(
+            <ScenarioSelector
+                onScenarioCrud="onScenarioCrud"
+                onScenarioSelect="onScenarioSelect"
+                height="50vh"
+                updateVars=""
+                active={false}
+            />
+        );
+        const elt = getByText("Add scenario");
+        expect(elt).toBeDisabled();
+    });
+    it("is enabled by default", async () => {
+        const { getByText } = render(
+            <ScenarioSelector
+                onScenarioCrud="onScenarioCrud"
+                onScenarioSelect="onScenarioSelect"
+                height="50vh"
+                updateVars=""
+            />
+        );
+        const elt = getByText("Add scenario");
+        expect(elt).not.toBeDisabled();
+    });
+    it("is enabled by active", async () => {
+        const { getByText } = render(
+            <ScenarioSelector
+                onScenarioCrud="onScenarioCrud"
+                onScenarioSelect="onScenarioSelect"
+                height="50vh"
+                updateVars=""
+                active={true}
+            />
+        );
+        const elt = getByText("Add scenario");
+        expect(elt).not.toBeDisabled();
+    });
+    it("dispatch a message at first render", async () => {
+        const dispatch = jest.fn();
+        const state = {};
+        render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ScenarioSelector
+                    onScenarioCrud="onScenarioCrud"
+                    onScenarioSelect="onScenarioSelect"
+                    height="50vh"
+                    updateVars=""
+                />
+            </TaipyContext.Provider>
+        );
+        await waitFor(() => expect(useDispatchRequestUpdateOnFirstRender).toHaveBeenCalled());
+    });
+    it("disables add scenario when reason", async () => {
+        const { getByText } = render(
+            <ScenarioSelector
+                onScenarioCrud="onScenarioCrud"
+                onScenarioSelect="onScenarioSelect"
+                height="50vh"
+                updateVars=""
+                creationNotAllowed="Because"
+            />
+        );
+        const elt = getByText("Add scenario");
+        expect(elt).toBeDisabled();
+    });
+});

+ 12 - 11
frontend/taipy/src/ScenarioSelector.tsx

@@ -11,11 +11,13 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { useEffect, useState, useCallback } from "react";
-import { Theme, Tooltip, alpha } from "@mui/material";
+import React, { useCallback, useEffect, useState } from "react";
 
 
+import { Theme, Tooltip, alpha } from "@mui/material";
+import { Add, Close, DeleteOutline, EditOutlined } from "@mui/icons-material";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
 import Button from "@mui/material/Button";
+import Dialog from "@mui/material/Dialog";
 import DialogActions from "@mui/material/DialogActions";
 import DialogActions from "@mui/material/DialogActions";
 import DialogContent from "@mui/material/DialogContent";
 import DialogContent from "@mui/material/DialogContent";
 import DialogTitle from "@mui/material/DialogTitle";
 import DialogTitle from "@mui/material/DialogTitle";
@@ -26,29 +28,28 @@ import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import IconButton from "@mui/material/IconButton";
 import InputLabel from "@mui/material/InputLabel";
 import InputLabel from "@mui/material/InputLabel";
 import MenuItem from "@mui/material/MenuItem";
 import MenuItem from "@mui/material/MenuItem";
-import Dialog from "@mui/material/Dialog";
 import Select from "@mui/material/Select";
 import Select from "@mui/material/Select";
 import Stack from "@mui/material/Stack";
 import Stack from "@mui/material/Stack";
 import TextField from "@mui/material/TextField";
 import TextField from "@mui/material/TextField";
 import Typography from "@mui/material/Typography";
 import Typography from "@mui/material/Typography";
-import { Close, DeleteOutline, Add, EditOutlined } from "@mui/icons-material";
-import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
+import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
 import { useFormik } from "formik";
 import { useFormik } from "formik";
 
 
 import {
 import {
-    useDispatch,
-    useModule,
     createSendActionNameAction,
     createSendActionNameAction,
-    getUpdateVar,
     createSendUpdateAction,
     createSendUpdateAction,
-    useDynamicProperty,
     getComponentClassName,
     getComponentClassName,
+    getUpdateVar,
+    useClassNames,
+    useDispatch,
+    useDynamicProperty,
+    useModule,
 } from "taipy-gui";
 } from "taipy-gui";
 
 
-import ConfirmDialog from "./utils/ConfirmDialog";
-import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx, CoreProps } from "./utils";
 import CoreSelector, { EditProps } from "./CoreSelector";
 import CoreSelector, { EditProps } from "./CoreSelector";
+import { CoreProps, MainTreeBoxSx, ScFProps, ScenarioFull, tinyIconButtonSx } from "./utils";
+import ConfirmDialog from "./utils/ConfirmDialog";
 import { Cycles, NodeType, Scenarios } from "./utils/types";
 import { Cycles, NodeType, Scenarios } from "./utils/types";
 
 
 type Property = {
 type Property = {

+ 1 - 1
frontend/taipy/src/ScenarioViewer.tsx

@@ -42,6 +42,7 @@ import {
     createSendActionNameAction,
     createSendActionNameAction,
     getComponentClassName,
     getComponentClassName,
     getUpdateVar,
     getUpdateVar,
+    useClassNames,
     useDispatch,
     useDispatch,
     useDynamicProperty,
     useDynamicProperty,
     useModule,
     useModule,
@@ -60,7 +61,6 @@ import {
     ScenarioFullLength,
     ScenarioFullLength,
     disableColor,
     disableColor,
     hoverSx,
     hoverSx,
-    useClassNames,
 } from "./utils";
 } from "./utils";
 import ConfirmDialog from "./utils/ConfirmDialog";
 import ConfirmDialog from "./utils/ConfirmDialog";
 import PropertiesEditor from "./PropertiesEditor";
 import PropertiesEditor from "./PropertiesEditor";

+ 1 - 4
frontend/taipy/src/utils.ts

@@ -14,7 +14,7 @@ import { Theme, alpha } from "@mui/material";
 import { PopoverOrigin } from "@mui/material/Popover";
 import { PopoverOrigin } from "@mui/material/Popover";
 import { ReactNode } from "react";
 import { ReactNode } from "react";
 
 
-import { getUpdateVar, useDynamicProperty } from "taipy-gui";
+import { getUpdateVar } from "taipy-gui";
 
 
 
 
 export interface CoreProps {
 export interface CoreProps {
@@ -153,9 +153,6 @@ export const tinyIconButtonSx = {
     },
     },
 };
 };
 
 
-export const useClassNames = (libClassName?: string, dynamicClassName?: string, className?: string) =>
-    ((libClassName || "") + " " + (useDynamicProperty(dynamicClassName, className, undefined) || "")).trim();
-
 export const disableColor = <T>(color: T, disabled: boolean) => (disabled ? ("disabled" as T) : color);
 export const disableColor = <T>(color: T, disabled: boolean) => (disabled ? ("disabled" as T) : color);
 
 
 export const hoverSx = {
 export const hoverSx = {

+ 16 - 0
frontend/taipy/test-config/Canvas.js

@@ -0,0 +1,16 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+HTMLCanvasElement.prototype.getContext = () => {
+    // return whatever getContext has to return
+  };

+ 19 - 0
frontend/taipy/test-config/createObjectUrl.js

@@ -0,0 +1,19 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+if (typeof window.URL.createObjectURL === 'undefined') {
+    window.URL.createObjectURL = () => {
+      // Do nothing
+      // Mock this function for plotly to work
+    };
+  }

+ 12 - 0
frontend/taipy/test-config/guiMock.js

@@ -0,0 +1,12 @@
+jest.mock("taipy-gui", () => ({
+    useDispatch: jest.fn(() => jest.fn()),
+    useModule: jest.fn(),
+    createSendActionNameAction: jest.fn(),
+    getUpdateVar: jest.fn((a, b) => b),
+    createSendUpdateAction: jest.fn(),
+    useDynamicProperty: jest.fn((a, b, c) => (typeof a == "undefined" ? (typeof b == "undefined" ? c : b) : a)),
+    getComponentClassName: jest.fn(),
+    useDispatchRequestUpdateOnFirstRender: jest.fn(),
+    useClassNames: jest.fn((a, b, c) => c || b || a || ""),
+    getSuffixedClassNames: jest.fn()
+}));

+ 23 - 0
frontend/taipy/test-config/intersectionObserver.js

@@ -0,0 +1,23 @@
+class IntersectionObserver {
+    root = null;
+    rootMargin = "";
+    thresholds = [];
+
+    disconnect() {
+      return null;
+    }
+
+    observe() {
+      return null;
+    }
+
+    takeRecords() {
+      return [];
+    }
+
+    unobserve() {
+      return null;
+    }
+  }
+  window.IntersectionObserver = IntersectionObserver;
+  global.IntersectionObserver = IntersectionObserver;

+ 19 - 0
frontend/taipy/test-config/jest.env.js

@@ -0,0 +1,19 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+// setup file for jest
+const dotenv = require('dotenv');
+const { TextEncoder, TextDecoder } = require('util');
+global.TextEncoder = TextEncoder;
+global.TextDecoder = TextDecoder;
+dotenv.config({ path: './.env.test' });

+ 18 - 0
frontend/taipy/test-config/nanoid.js

@@ -0,0 +1,18 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+// mock nanoid that is ESM and does not work with jest
+// https://github.com/ai/nanoid/issues/363
+jest.mock("nanoid", () => ({
+    nanoid : ()=>{}
+  }), {virtual: true});

+ 1 - 5
taipy/gui_core/_context.py

@@ -516,11 +516,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             finally:
             finally:
                 self.scenario_refresh(scenario_id)
                 self.scenario_refresh(scenario_id)
                 if (scenario or user_scenario) and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
                 if (scenario or user_scenario) and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
-                    try:
-                        var_name, _ = gui._get_real_var_name(sel_scenario_var)
-                        self.gui._update_var(var_name, scenario or user_scenario, on_change=args[2])
-                    except Exception as e:  # pragma: no cover
-                        _warn("Can't find value variable name in context", e)
+                    self.gui._update_var(sel_scenario_var, scenario or user_scenario, on_change=args[2])
         if scenario:
         if scenario:
             if not (reason := is_editable(scenario)):
             if not (reason := is_editable(scenario)):
                 state.assign(error_var, f"Scenario {scenario_id or name} is not editable: {_get_reason(reason)}.")
                 state.assign(error_var, f"Scenario {scenario_id or name} is not editable: {_get_reason(reason)}.")

+ 5 - 2
tools/frontend/hash_source.py

@@ -42,8 +42,11 @@ def hash_files_in_frontend_folder(frontend_folder):
     combined_hasher = hashlib.sha256()
     combined_hasher = hashlib.sha256()
     file_hashes = {}
     file_hashes = {}
     lookup_fe_folder = [f"{frontend_folder}{os.sep}taipy", f"{frontend_folder}{os.sep}taipy-gui"]
     lookup_fe_folder = [f"{frontend_folder}{os.sep}taipy", f"{frontend_folder}{os.sep}taipy-gui"]
-    if len(sys.argv) > 1 and sys.argv[1] == "--taipy-gui-only":
-        lookup_fe_folder.pop(0)
+    if len(sys.argv) > 1:
+        if sys.argv[1] == "--taipy-gui-only":
+            lookup_fe_folder.pop(0)
+        elif sys.argv[1] == "--taipy-gui-core-only":
+            lookup_fe_folder.pop(1)
     for root_folder in lookup_fe_folder:
     for root_folder in lookup_fe_folder:
         # Sort before looping to ensure consistent cache key
         # Sort before looping to ensure consistent cache key
         for root, _, files in sorted(os.walk(root_folder)):
         for root, _, files in sorted(os.walk(root_folder)):

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott