Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUI menus and basic layout. #102

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions source/titfront/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause.
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

import { FC } from "react";
import { AiOutlinePython as PythonIcon } from "react-icons/ai";
import {

Check warning on line 8 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L7-L8

Added lines #L7 - L8 were not covered by tests
FiActivity as ActivityIcon,
FiDatabase as DatabaseIcon,
FiSettings as SettingsIcon,
FiSliders as SlidersIcon,
FiTerminal as TerminalIcon,
} from "react-icons/fi";

import { Menu } from "~/components/Menu";
import { PythonShell } from "~/components/PythonShell";
import { Viewer } from "~/components/Viewer";

Check warning on line 18 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L16-L18

Added lines #L16 - L18 were not covered by tests

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

export const App: FC = () => {

Check warning on line 22 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L22

Added line #L22 was not covered by tests
const leftIconSize = 24;
const bottomIconSize = 16;
Comment on lines +23 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider extracting icon sizes into constants or theme configuration.

The hardcoded icon sizes (leftIconSize = 24 and bottomIconSize = 16) should be moved to a theme configuration to maintain consistency across the application.

+// In a new file: source/titfront/src/theme/constants.ts
+export const ICON_SIZES = {
+  menuLeft: 24,
+  menuBottom: 16,
+} as const;

-  const leftIconSize = 24;
-  const bottomIconSize = 16;
+  const { menuLeft: leftIconSize, menuBottom: bottomIconSize } = ICON_SIZES;

Also applies to: 25-25

return (
<div className="h-screen w-screen flex flex-row select-none text-sm">
<Menu side="left">
<Menu.Item
name="Configuration"
icon={<SlidersIcon size={leftIconSize} />}
group={0}
/>
<Menu.Item
name="Storage"
icon={<DatabaseIcon size={leftIconSize} />}
group={0}
/>
<Menu.Item
name="Activity"
icon={<ActivityIcon size={leftIconSize} />}
group={1}
/>
<Menu.Item
name="Settings"
icon={<SettingsIcon size={leftIconSize} />}
group={1}

Check warning on line 46 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L26-L46

Added lines #L26 - L46 were not covered by tests
/>
</Menu>
Comment on lines +27 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider extracting menu items configuration.

The menu items are currently hardcoded in the component. Consider extracting them into a configuration object for better maintainability and reusability.

+// In a new file: source/titfront/src/config/menuConfig.ts
+import { IconType } from 'react-icons';
+import { SlidersIcon, DatabaseIcon, ActivityIcon, SettingsIcon } from "react-icons/fi";
+
+interface MenuItem {
+  name: string;
+  icon: IconType;
+  group: number;
+}
+
+export const LEFT_MENU_ITEMS: MenuItem[] = [
+  { name: "Configuration", icon: SlidersIcon, group: 0 },
+  { name: "Storage", icon: DatabaseIcon, group: 0 },
+  { name: "Activity", icon: ActivityIcon, group: 1 },
+  { name: "Settings", icon: SettingsIcon, group: 1 },
+];

Also applies to: 51-65

<div className="flex-1 flex flex-col">
<Viewer />
<Menu side="bottom">
<Menu.Item
name="Python shell"
icon={<PythonIcon size={bottomIconSize} />}
group={0}

Check warning on line 55 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L48-L55

Added lines #L48 - L55 were not covered by tests
>
<PythonShell />
</Menu.Item>
<Menu.Item
name="Console"
icon={<TerminalIcon size={bottomIconSize} />}
group={0}

Check warning on line 62 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L57-L62

Added lines #L57 - L62 were not covered by tests
/>
</Menu>
</div>
</div>

Check warning on line 66 in source/titfront/src/components/App.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/App.tsx#L64-L66

Added lines #L64 - L66 were not covered by tests
);
};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
191 changes: 191 additions & 0 deletions source/titfront/src/components/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause.
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

import {

Check warning on line 6 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L6

Added line #L6 was not covered by tests
FC,
Fragment,
ReactNode,
ReactElement,
createContext,
useContext,
useMemo,
} from "react";
import { IconBaseProps } from "react-icons";
import { FiMinimize as MinimizeIcon } from "react-icons/fi";

Check warning on line 16 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L16

Added line #L16 was not covered by tests

import { Resizable } from "~/components/Resizable";
import { useMenuStore } from "~/stores/LayoutStore";
import { Side, iota, cn } from "~/utils";

Check warning on line 20 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L18-L20

Added lines #L18 - L20 were not covered by tests

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

interface Action {
key: string;
icon: ReactElement<IconBaseProps>;
callback: () => void;
}

export interface MenuProviderProps {
actions: Action[];
}

const MenuContext = createContext<MenuProviderProps | null>(null);

Check warning on line 34 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L34

Added line #L34 was not covered by tests

export function useMenu() {
return useContext(MenuContext)!;

Check warning on line 37 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L36-L37

Added lines #L36 - L37 were not covered by tests
}
Comment on lines +41 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove non-null assertion and add error handling.

The useMenu hook uses a non-null assertion which could lead to runtime errors if the context is null.

 export function useMenu() {
-  return useContext(MenuContext)!;
+  const context = useContext(MenuContext);
+  if (context === null) {
+    throw new Error("useMenu must be used within a MenuContext.Provider");
+  }
+  return context;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function useMenu() {
return useContext(MenuContext)!;
}
export function useMenu() {
const context = useContext(MenuContext);
if (context === null) {
throw new Error("useMenu must be used within a MenuContext.Provider");
}
return context;
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 37-37: Forbidden non-null assertion.

(lint/style/noNonNullAssertion)

🪛 GitHub Check: codecov/patch

[warning] 36-37: source/titfront/src/components/Menu.tsx#L36-L37
Added lines #L36 - L37 were not covered by tests


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

interface MenuProps {
side: Side;
children: ReactElement<MenuItemProps>[];
}

export const Menu: FC<MenuProps> & { Item: FC<MenuItemProps> } = ({

Check warning on line 47 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L47

Added line #L47 was not covered by tests
side,
children = [],
}: MenuProps) => {
const { size, setSize, activeItem, setActiveItem } = useMenuStore(side);

Check warning on line 51 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L49-L51

Added lines #L49 - L51 were not covered by tests

/** @todo We should have a way to add custom actions from the children. */
Copy link

@coderabbitai coderabbitai bot Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Address the TODO comment about custom actions.

The comment indicates a need for custom actions from children. Consider implementing this feature or creating an issue to track it.

Would you like me to help implement custom actions support or create an issue to track this task?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please, make a sketch on how this can be done. I want the actions to be added from withing the child of a Menu.Item.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jhuighuy Here's a sketch of how we can implement custom actions from children:

// Modified context interface
interface MenuContextProps {
  actions: Action[];
  registerAction: (action: Action) => void;
  unregisterAction: (key: string) => void;
}

// Updated Menu component
export const Menu: FC<MenuProps> & { Item: FC<MenuItemProps> } = ({
  side,
  children = [],
}: MenuProps) => {
  const { size, setSize, activeItem, setActiveItem } = useMenuStore(side);
  const [customActions, setCustomActions] = useState<Action[]>([]);

  const registerAction = useCallback((action: Action) => {
    setCustomActions(prev => [...prev, action]);
  }, []);

  const unregisterAction = useCallback((key: string) => {
    setCustomActions(prev => prev.filter(action => action.key !== key));
  }, []);

  const contextValue = useMemo(() => ({
    actions: [
      // Default minimize action
      {
        key: "minimize",
        icon: <MinimizeIcon />,
        callback: () => setActiveItem(-1),
      },
      ...customActions,
    ],
    registerAction,
    unregisterAction,
  }), [customActions, setActiveItem]);

  return (
    <MenuContext.Provider value={contextValue}>
      {/* ... rest of the component ... */}
    </MenuContext.Provider>
  );
};

// Example usage in a child component
const MyCustomMenuItem = () => {
  const { registerAction, unregisterAction } = useMenu();

  useEffect(() => {
    const action = {
      key: "custom-action",
      icon: <CustomIcon />,
      callback: () => {
        // Custom action logic
      },
    };
    registerAction(action);
    return () => unregisterAction(action.key);
  }, []);

  return <div>Custom Menu Item</div>;
};

// Usage
<Menu side="left">
  <Menu.Item>
    <MyCustomMenuItem />
  </Menu.Item>
</Menu>

This approach:

  1. Allows children to register/unregister actions dynamically
  2. Maintains type safety
  3. Cleans up actions when components unmount
  4. Preserves the default minimize action

Would you like me to explain any part in more detail?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we encapsulate the logic of registering and unregistering into a custom hook?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jhuighuy Here's how we can encapsulate the logic into a custom hook:

// Custom hook for action management
const useMenuAction = (action: Action) => {
  const { registerAction, unregisterAction } = useMenu();
  
  useEffect(() => {
    registerAction(action);
    return () => unregisterAction(action.key);
  }, [action]);
};

// Example usage in a child component
const MyCustomMenuItem = () => {
  useMenuAction({
    key: "custom-action",
    icon: <CustomIcon />,
    callback: () => {
      // Custom action logic
    },
  });

  return <div>Custom Menu Item</div>;
};

This simplifies the usage and ensures consistent cleanup. Would you like me to show how to handle dynamic action updates as well?

const actions = useMemo<MenuProviderProps>(
() => ({

Check warning on line 55 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L54-L55

Added lines #L54 - L55 were not covered by tests
actions: [
{
key: "hide",
icon: <MinimizeIcon size={16} />,
callback: () => setActiveItem(-1),

Check warning on line 60 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L58-L60

Added lines #L58 - L60 were not covered by tests
},
],
}),

Check warning on line 63 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L63

Added line #L63 was not covered by tests
[setActiveItem]
);

const maxGroup = children.reduce(
(max, child) => Math.max(max, child.props.group),

Check warning on line 68 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L67-L68

Added lines #L67 - L68 were not covered by tests
0
);

Check warning on line 70 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L70

Added line #L70 was not covered by tests

const horizontal = side === "left" || side === "right";
const menuCn = cn(
"flex text-gray-300",

Check warning on line 74 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L72-L74

Added lines #L72 - L74 were not covered by tests
{
left: "flex-row",
right: "flex-row-reverse",
top: "flex-col",
bottom: "flex-col-reverse",
}[side]
);
const menuBarCn = cn(
"flex items-center justify-between",
"bg-gradient-to-bl from-gray-800 to-gray-950",
horizontal ? "flex-col w-16 h-full gap-6" : "w-full h-8 pl-1"
);
const baseItemCn = cn(
"flex items-center",
horizontal
? "justify-center size-11 my-6 rounded-lg"
: "w-30 h-6 pl-1 rounded-sm"
);
const itemCn = cn(
baseItemCn,
"hover:bg-gradient-to-bl hover:from-gray-700 hover:to-gray-800",
"hover:text-gray-100 hover:shadow-xl"
);
const activeItemCn = cn(
baseItemCn,
"inset-shadow-sm inset-shadow-gray-700",
"bg-gradient-to-bl from-gray-700 to-indigo-900 text-gray-100"
);

Check warning on line 102 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L76-L102

Added lines #L76 - L102 were not covered by tests
return (
<div className={menuCn}>

Check warning on line 104 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L104

Added line #L104 was not covered by tests
{/* Menu bar. */}
<div className={menuBarCn}>
{iota(maxGroup + 1).map((group) => (
<div key={group} className={cn(horizontal || "flex items-center")}>
{children.map(

Check warning on line 109 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L106-L109

Added lines #L106 - L109 were not covered by tests
(item, index) =>
item.props.group == group && (
<Fragment key={index}>
<button
className={index === activeItem ? activeItemCn : itemCn}
onClick={() => setActiveItem(index)}

Check warning on line 115 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L111-L115

Added lines #L111 - L115 were not covered by tests
>
{item.props.icon}
{horizontal || (
<span className="ml-1">{item.props.name}</span>

Check warning on line 119 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L117-L119

Added lines #L117 - L119 were not covered by tests
)}
</button>
Comment on lines +135 to +143
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add type attributes to button elements.

The button elements should have explicit type attributes to prevent form submission behavior.

 <button
   className={index === activeItem ? activeItemCn : itemCn}
   onClick={() => setActiveItem(index)}
+  type="button"
 >

 <button
   key={key}
   className={cn(
     "w-6 h-6 mr-2 rounded flex items-center justify-center",
     "hover:bg-gray-500"
   )}
   onClick={callback}
+  type="button"
 >

Also applies to: 166-176

🧰 Tools
🪛 Biome (1.9.4)

[error] 113-116: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🪛 GitHub Check: codecov/patch

[warning] 117-119: source/titfront/src/components/Menu.tsx#L117-L119
Added lines #L117 - L119 were not covered by tests

{horizontal || (
<div className="w-[1px] h-6 mx-1 bg-gray-600" />

Check warning on line 123 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L121-L123

Added lines #L121 - L123 were not covered by tests
)}
</Fragment>

Check warning on line 125 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L125

Added line #L125 was not covered by tests
)
)}
</div>
))}
</div>

Check warning on line 130 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L127-L130

Added lines #L127 - L130 were not covered by tests
{/* Active item. */}
{activeItem !== -1 && (
<Resizable side={side} size={size} setSize={setSize}>
<MenuContext.Provider value={actions}>
{children[activeItem]}
</MenuContext.Provider>
</Resizable>
)}
</div>

Check warning on line 139 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L132-L139

Added lines #L132 - L139 were not covered by tests
);
};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

interface MenuItemProps {
name: string;
icon: ReactElement<IconBaseProps>;
group: number;
children?: ReactNode;
}

const MenuItem: FC<MenuItemProps> = ({ name, children }) => {
const { actions } = useMenu();

Check warning on line 153 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L152-L153

Added lines #L152 - L153 were not covered by tests
return (
<div
className={cn(
"size-full flex flex-col",
"bg-gradient-to-bl from-gray-700 to-gray-800"
)}

Check warning on line 159 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L155-L159

Added lines #L155 - L159 were not covered by tests
>
{/* Title. */}
<div className="h-4 m-1 mt-2 flex flex-row items-center justify-between">
<span className="ml-2 font-medium truncate">{name.toUpperCase()}</span>
<div className="flex flex-row items-center">
{actions.map(({ key, icon, callback }) => (
<button
key={key}
className={cn(
"w-6 h-6 mr-2 rounded flex items-center justify-center",
"hover:bg-gray-500"
)}
onClick={callback}

Check warning on line 172 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L162-L172

Added lines #L162 - L172 were not covered by tests
>
{icon}
</button>
))}
</div>
</div>

Check warning on line 178 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L174-L178

Added lines #L174 - L178 were not covered by tests
{/* Contents. */}
<div
className={cn("flex-grow m-1 rounded-lg overflow-auto", "bg-gray-900")}

Check warning on line 181 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L180-L181

Added lines #L180 - L181 were not covered by tests
>
{children}
</div>
</div>

Check warning on line 185 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L183-L185

Added lines #L183 - L185 were not covered by tests
);
};

Menu.Item = MenuItem;

Check warning on line 189 in source/titfront/src/components/Menu.tsx

View check run for this annotation

Codecov / codecov/patch

source/titfront/src/components/Menu.tsx#L189

Added line #L189 was not covered by tests

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 changes: 1 addition & 1 deletion source/titfront/src/components/PythonShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const PythonShell: FC = () => {
data-testid="python-shell"
className={cn(
"size-full flex flex-col overflow-auto",
"font-mono select-text bg-gray-900"
"font-mono select-text"
)}
aria-label="Python Shell"
onClick={handleClick}
Expand Down
123 changes: 123 additions & 0 deletions source/titfront/src/components/Resizable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause.
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

import "@testing-library/jest-dom/vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { vi } from "vitest";

import { Resizable } from "~/components/Resizable";

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

describe("Resizable", () => {
it("renders children correctly", () => {
const { getByText } = render(
<Resizable side="left" size={200} setSize={() => {}}>
<div>Test</div>
</Resizable>
);
expect(getByText("Test")).toBeInTheDocument();
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

it("applies initial width correctly", () => {
render(
<Resizable side="left" size={200} setSize={() => {}}>
<div>Test</div>
</Resizable>
);
expect(screen.getByTestId("resizable-content")).toHaveStyle("width: 200px");
});

it("applies initial height correctly", () => {
render(
<Resizable side="bottom" size={200} setSize={() => {}}>
<div>Test</div>
</Resizable>
);
expect(screen.getByTestId("resizable-content")).toHaveStyle(
"height: 200px"
);
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

it("handles width changes with mouse correctly", () => {
const setSize = vi.fn();
render(
<Resizable side="left" size={200} setSize={setSize}>
<div>Test Content</div>
</Resizable>
);

const resizer = screen.getByTestId("resizable-resizer");

fireEvent.mouseDown(resizer, { clientX: 0 });
fireEvent.mouseMove(window, { clientX: 50 });
expect(document.body.style.cursor).toBe("ew-resize");
expect(setSize).toHaveBeenCalledWith(250);

fireEvent.mouseUp(window);
expect(document.body.style.cursor).toBe("");
});

it("handles height changes with mouse correctly", () => {
const setSize = vi.fn();
render(
<Resizable side="bottom" size={200} setSize={setSize}>
<div>Test Content</div>
</Resizable>
);
const resizer = screen.getByTestId("resizable-resizer");

fireEvent.mouseDown(resizer, { clientY: 0 });
fireEvent.mouseMove(window, { clientY: -50 });
expect(document.body.style.cursor).toBe("ns-resize");
expect(setSize).toHaveBeenCalledWith(250);

fireEvent.mouseUp(window);
expect(document.body.style.cursor).toBe("");
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

it("respects size constraints", () => {
const setSize = vi.fn();
render(
<Resizable
side="left"
size={200}
setSize={setSize}
minSize={100}
maxSize={300}
>
<div>Test Content</div>
</Resizable>
);
const resizer = screen.getByTestId("resizable-resizer");

fireEvent.mouseDown(resizer, { clientX: 0 });
fireEvent.mouseMove(window, { clientX: 50 });
expect(setSize).toHaveBeenCalledWith(250);

fireEvent.mouseMove(window, { clientX: 150 });
expect(setSize).toHaveBeenCalledWith(300);

fireEvent.mouseUp(window);

fireEvent.mouseDown(resizer, { clientX: 0 });
fireEvent.mouseMove(window, { clientX: -50 });
expect(setSize).toHaveBeenCalledWith(150);

fireEvent.mouseMove(window, { clientX: -150 });
expect(setSize).toHaveBeenCalledWith(100);

fireEvent.mouseUp(window);
});
});

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Loading
Loading