From 149b82cb66630e3589960a7bad87513f9f04dbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Sun, 4 Jan 2026 18:29:19 +0100 Subject: [PATCH] feat: create tests, more integration testing, fix ID tests, use UUID (almost) everywhere ref: N25B-412 --- src/pages/VisProgPage/VisProg.module.css | 72 +--------- .../components/GestureValueEditor.tsx | 90 +++++++----- .../visualProgrammingUI/components/Plan.tsx | 1 - .../components/PlanEditor.module.css | 71 ++++++++++ .../components/PlanEditor.tsx | 19 ++- .../components/DragDropSidebar.test.tsx | 6 +- .../components/GestureValueEditor.test.tsx | 131 ++++++++++++++++++ .../components/PlanEditor.test.tsx | 50 +++---- .../nodes/NormNode.test.tsx | 18 ++- .../nodes/PhaseNode.test.tsx | 6 +- test/setupFlowTests.ts | 14 ++ 11 files changed, 332 insertions(+), 146 deletions(-) create mode 100644 src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor.module.css create mode 100644 test/pages/visProgPage/visualProgrammingUI/components/GestureValueEditor.test.tsx diff --git a/src/pages/VisProgPage/VisProg.module.css b/src/pages/VisProgPage/VisProg.module.css index e15db1f..429e740 100644 --- a/src/pages/VisProgPage/VisProg.module.css +++ b/src/pages/VisProgPage/VisProg.module.css @@ -140,78 +140,8 @@ filter: drop-shadow(0 0 0.25rem plum); } -.planDialog { - overflow:visible; - width: 80vw; - max-width: 900px; - transition: width 0.25s ease; -} - - -.planDialog::backdrop { - background: rgba(0, 0, 0, 0.4); -} - -.planEditor { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - min-width: 600px; -} - -.planEditorLeft { - position: relative; - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.planEditorRight { - display: flex; - flex-direction: column; - gap: 0.5rem; - border-left: 1px solid var(--border-color, #ccc); - padding-left: 1rem; - max-height: 300px; - overflow-y: auto; -} - -.planStep { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - transition: text-decoration 0.2s; -} - - -.planStep:hover { - text-decoration: line-through; -} - -.stepType { - opacity: 0.7; - font-size: 0.85em; -} - - -.stepIndex { - opacity: 0.6; -} - -.emptySteps { - opacity: 0.5; - font-style: italic; -} - -.stepSuggestion { - opacity: 0.5; - font-style: italic; -} - .planNoIterate { opacity: 0.5; font-style: italic; text-decoration: line-through; -} - +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor.tsx index 5cb76a4..67f9f16 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor.tsx @@ -1,13 +1,23 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useRef } from "react"; import styles from './GestureValueEditor.module.css' +/** + * Props for the GestureValueEditor component. + * - value: current gesture value (controlled by parent) + * - setValue: callback to update the gesture value in parent state + * - placeholder: optional placeholder text for the input field + */ type GestureValueEditorProps = { value: string; setValue: (value: string) => void; placeholder?: string; }; -// Define your gesture tags here +/** + * List of high-level gesture "tags". + * These are human-readable categories or semantic labels. + * In a real app, these would likely be loaded from an external source. + */ const GESTURE_TAGS = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any", "assuage", "attemper", "back", "bashful", "beg", "beseech", "blank", "body language", "bored", "bow", "but", "call", "calm", "choose", "choice", "cloud", @@ -23,6 +33,11 @@ const GESTURE_TAGS = ["above", "affirmative", "afford", "agitated", "all", "allr "think", "timid", "top", "unless", "up", "upstairs", "void", "warm", "winner", "yeah", "yes", "yoo-hoo", "you", "your", "zero", "zestful"]; +/** + * List of concrete gesture animation paths. + * These represent specific animation assets and are used in "single" mode + * with autocomplete-style selection, also would be loaded from an external source. + */ const GESTURE_SINGLES = [ "animations/Stand/BodyTalk/Listening/Listening_1", "animations/Stand/BodyTalk/Listening/Listening_2", @@ -421,50 +436,62 @@ const GESTURE_SINGLES = [ "animations/Stand/Waiting/Zombie_1"] +/** + * Returns a gesture value editor component. + * @returns JSX.Element + */ export default function GestureValueEditor({ value, setValue, placeholder = "Gesture name", }: GestureValueEditorProps) { + + /** Input mode: semantic tag vs concrete animation path */ const [mode, setMode] = useState<"single" | "tag">("tag"); + + /** Raw text value for single-gesture input */ const [customValue, setCustomValue] = useState(""); + + /** Autocomplete dropdown state */ const [showSuggestions, setShowSuggestions] = useState(true); const [filteredSuggestions, setFilteredSuggestions] = useState([]); + + /** Reserved for future click-outside / positioning logic */ const containerRef = useRef(null); + /** Switch between tag and single input modes */ const handleModeChange = (newMode: "single" | "tag") => { setMode(newMode); + if (newMode === "single") { - // When switching to single, use custom value or existing value setValue(customValue || value); setFilteredSuggestions(GESTURE_SINGLES); setShowSuggestions(true); } else { - // When switching to tag, clear value if not a valid tag - const isCurrentValueTag = GESTURE_TAGS.some(tag => - tag.toLowerCase() === value.toLowerCase() + // Clear value if it does not match a valid tag + const isValidTag = GESTURE_TAGS.some( + tag => tag.toLowerCase() === value.toLowerCase() ); - if (!isCurrentValueTag) { - setValue(""); - } + if (!isValidTag) setValue(""); setShowSuggestions(false); } }; + /** Select a semantic gesture tag */ const handleTagSelect = (tag: string) => { setValue(tag); }; + /** Update single-gesture input and filter suggestions */ const handleCustomChange = (newValue: string) => { setCustomValue(newValue); setValue(newValue); - - // Filter suggestions based on input + if (newValue.trim() === "") { - setShowSuggestions(true) setFilteredSuggestions(GESTURE_SINGLES); + setShowSuggestions(true); } else { - const filtered = GESTURE_SINGLES.filter(single => + const filtered = GESTURE_SINGLES.filter(single => single.toLowerCase().includes(newValue.toLowerCase()) ); setFilteredSuggestions(filtered); @@ -472,30 +499,32 @@ export default function GestureValueEditor({ } }; + /** Commit autocomplete selection */ const handleSuggestionSelect = (suggestion: string) => { setCustomValue(suggestion); setValue(suggestion); setShowSuggestions(false); }; + /** Refresh suggestions on refocus */ const handleInputFocus = () => { - if (customValue.trim() !== "") { - const filtered = GESTURE_SINGLES.filter(tag => - tag.toLowerCase().includes(customValue.toLowerCase()) - ); - setFilteredSuggestions(filtered); - setShowSuggestions(filtered.length > 0); - } + if (!customValue.trim()) return; + + const filtered = GESTURE_SINGLES.filter(single => + single.toLowerCase().includes(customValue.toLowerCase()) + ); + setFilteredSuggestions(filtered); + setShowSuggestions(filtered.length > 0); }; - const handleInputBlur = (_e: React.FocusEvent) => { - // Delay hiding suggestions to allow clicking on them + /** Exists to allow delayed blur handling if needed */ + const handleInputBlur = (_e: React.FocusEvent) => {}; - }; + /** Build the JSX component */ return (
- {/* Mode selector */} + {/* Mode toggle */}
@@ -516,8 +545,7 @@ export default function GestureValueEditor({
- {/* Value editor based on mode */} -
+
{mode === "single" ? (
{showSuggestions && ( @@ -527,7 +555,7 @@ export default function GestureValueEditor({ key={suggestion} className={styles.suggestionItem} onClick={() => handleSuggestionSelect(suggestion)} - onMouseDown={(e) => e.preventDefault()} + onMouseDown={(e) => e.preventDefault()} // prevent blur before click > {suggestion}
@@ -551,14 +579,14 @@ export default function GestureValueEditor({ value={value} onChange={(e) => handleTagSelect(e.target.value)} className={styles.tagSelect} + data-testid={"tagSelectorTestID"} > - + {GESTURE_TAGS.map((tag) => ( - + ))} +
{GESTURE_TAGS.map((tag) => (