Skip to content
This repository was archived by the owner on Oct 3, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 21 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import CodeExample from "./components/CodeExample";
import { PROJECT_ID } from "./config";

import commands from "./commands.json";
import DesignDebugger from "./theme/debug/DesignDebugger";

function App() {
const [environment, setEnvironment] = useState<string | null>(null);
Expand Down Expand Up @@ -107,9 +108,28 @@ services:
}
}, [environment, sessionStorageType]);

const debugEnabled =
process.env.REACT_APP_ENABLE_DESIGN_DEBUG &&
process.env.REACT_APP_ENABLE_DESIGN_DEBUG !== "false" &&
process.env.REACT_APP_ENABLE_DESIGN_DEBUG !== "0";

const DesignDebug = () => {
return debugEnabled ? (
<DesignDebugger
defaultEnvironment={environment}
defaultStorage={sessionStorageType}
defaultErrorState={fatalErrorMessage}
onEnvironmentChange={(environment) => setEnvironment(environment)}
onStorageChange={(storageType) => setSessionStorageType(storageType)}
onErrorChange={(errorState) => setFatalErrorMessage(errorState)}
/>
) : null;
};

if (fatalErrorMessage)
return (
<ErrorPage header="We cannot fetch your data">
<DesignDebug />
<p className="mt-2 mb-2">
{" "}
There was an error fetching data from your Python backend at{" "}
Expand All @@ -128,6 +148,7 @@ services:

return (
<>
<DesignDebug />
<div
className={`max-w-7xl w-fill px-6 2xl:pl-0 m-auto transition duration-500`}
>
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/theme/debug/DesignDebugger.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from "react";
import { render, fireEvent, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import DesignDebugger from "./DesignDebugger";

describe("DesignDebugger", () => {
// Test for initial state based on default props
it("initializes with default props", () => {
const defaultProps = {
defaultEnvironment: "Development",
defaultStorage: "redis",
defaultErrorState: null,
};
render(<DesignDebugger {...defaultProps} />);

// @ts-ignore
expect(screen.getByTestId("environmentInput").value).toBe(
defaultProps.defaultEnvironment,
);
// @ts-ignore
expect(screen.getByTestId("sessionStorageTypeInput").value).toBe(
defaultProps.defaultStorage,
);
expect(screen.getByTestId("fatalErrorInput")).not.toBeChecked();
});

// Test for environment change functionality
it("changes environment selection", () => {
const mockEnvironmentChange = jest.fn();
render(<DesignDebugger onEnvironmentChange={mockEnvironmentChange} />);

const select = screen.getByTestId("environmentInput");
fireEvent.change(select, { target: { value: "Staging" } });

// @ts-ignore
expect(select.value).toBe("Staging");
expect(mockEnvironmentChange).toHaveBeenCalledWith("Staging");
});

// Test for storage change functionality
it("changes session storage selection", () => {
const mockStorageChange = jest.fn();
render(<DesignDebugger onStorageChange={mockStorageChange} />);

const select = screen.getByTestId("sessionStorageTypeInput");
fireEvent.change(select, { target: { value: "file" } });

// @ts-ignore
expect(select.value).toBe("file");
expect(mockStorageChange).toHaveBeenCalledWith("file");
});

// Test for error emulation checkbox
it("toggles error emulation", () => {
const mockErrorChange = jest.fn();
render(<DesignDebugger onErrorChange={mockErrorChange} />);

const checkbox = screen.getByTestId("fatalErrorInput");
fireEvent.click(checkbox);

expect(checkbox).toBeChecked();
expect(mockErrorChange).toHaveBeenCalledWith("Emulated error message");

fireEvent.click(checkbox);

expect(checkbox).not.toBeChecked();
expect(mockErrorChange).toHaveBeenCalledWith(null);
});
});
89 changes: 89 additions & 0 deletions frontend/src/theme/debug/DesignDebugger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useState } from "react";
import Draggable from "./Draggable";

interface DesignDebuggerProps {
defaultEnvironment?: string | null;
defaultStorage?: string | null;
defaultErrorState?: string | null;
onEnvironmentChange?: (environment: string | null) => void;
onStorageChange?: (storageType: string | null) => void;
onErrorChange?: (error: string | null) => void;
}

const DesignDebugger: React.FC<DesignDebuggerProps> = (props) => {
const [environment, setEnvironment] = useState<string | null>(
props.defaultEnvironment || "production",
);
const [sessionStorageType, setSessionStorageType] = useState<string | null>(
props.defaultStorage || "file",
);
const [fatalErrorMessage, setFatalErrorMessage] = useState<string | null>(
props.defaultErrorState || null,
);

const handleEnvironmentChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setEnvironment(value);
props.onEnvironmentChange && props.onEnvironmentChange(value);
};

const handleStorageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setSessionStorageType(value);
props.onStorageChange && props.onStorageChange(value);
};

const handleErrorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const errorMessage = e.target.checked ? "Emulated error message" : null;
setFatalErrorMessage(errorMessage);
props.onErrorChange && props.onErrorChange(errorMessage);
};

return (
<Draggable>
<div className="flex flex-col space-y-4 p-4 bg-gray-100 rounded-md shadow-sm">
<div className="flex items-center space-x-4">
<label className="text-gray-700 font-medium">Environment:</label>
<select
data-testid={"environmentInput"}
value={environment || ""}
onChange={handleEnvironmentChange}
className="ml-2 border p-2 rounded-md text-gray-600"
>
<option value="Production">Production</option>
<option value="Staging">Staging</option>
<option value="Development">Development</option>
</select>
</div>

<div className="flex items-center space-x-4">
<label className="text-gray-700 font-medium">Session Storage:</label>
<select
data-testid={"sessionStorageTypeInput"}
value={sessionStorageType || ""}
onChange={handleStorageChange}
className="ml-2 border p-2 rounded-md text-gray-600"
>
<option value="file">File</option>
<option value="redis">Redis</option>
</select>
</div>

<div className="flex items-center space-x-4">
<label className="text-gray-700 font-medium mr-2">
Emulate Error:
</label>
<input
data-testid={"fatalErrorInput"}
type="checkbox"
checked={fatalErrorMessage !== null}
onChange={handleErrorChange}
className="border rounded-md"
/>
</div>
</div>
</Draggable>
);
};

export default DesignDebugger;
66 changes: 66 additions & 0 deletions frontend/src/theme/debug/Draggable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import { render, fireEvent, screen } from "@testing-library/react";
import Draggable from "./Draggable";

describe("Draggable", () => {
beforeEach(() => {
Storage.prototype.getItem = jest.fn(() => JSON.stringify({ x: 50, y: 50 }));
Storage.prototype.setItem = jest.fn();
});

it("renders children", () => {
render(
<Draggable>
<div>Test Child</div>
</Draggable>,
);
expect(screen.getByText("Test Child")).toBeInTheDocument();
});

it("updates position on drag", () => {
render(
<Draggable>
<div>Test Child</div>
</Draggable>,
);
const draggableElement = screen.getByTestId("draggable");

// Simulate mouse down
fireEvent.mouseDown(draggableElement, { clientX: 50, clientY: 50 });

// Simulate mouse move
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });

// Simulate mouse up
fireEvent.mouseUp(document);

// Check if the position was updated
expect(draggableElement.style.left).toBe("50px");
expect(draggableElement.style.top).toBe("50px");
});

it("saves position to localStorage on mouse up", () => {
// Set initial position
Storage.prototype.getItem = jest.fn(() => JSON.stringify({ x: 0, y: 0 }));

render(
<Draggable>
<div>Test Child</div>
</Draggable>,
);
const draggableElement = screen.getByTestId("draggable");

// Simulate a drag operation
fireEvent.mouseDown(draggableElement, { clientX: 0, clientY: 0 });
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
fireEvent.mouseUp(document);

// Assuming the draggable element updates its position after a drag,
// the new position would be something like { x: 100, y: 100 }
// This will depend on how your component calculates the new position
expect(localStorage.setItem).toHaveBeenCalledWith(
"draggablePosition",
JSON.stringify({ x: 100, y: 100 }),
);
});
});
77 changes: 77 additions & 0 deletions frontend/src/theme/debug/Draggable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useState, useRef, useEffect } from "react";

interface DraggableProps {
children: React.ReactNode;
}

const Draggable: React.FC<DraggableProps> = ({ children }) => {
const initialPosition = JSON.parse(
localStorage.getItem("draggablePosition") || '{ "x": 0, "y": 0 }',
);
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState(initialPosition);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const ref = useRef<HTMLDivElement>(null);

const onMouseDown = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setOffset({
x: event.clientX - rect.left,
y: event.clientY - rect.top,
});
}
setIsDragging(true);
};

useEffect(() => {
const onMouseUp = () => {
setIsDragging(false);
localStorage.setItem("draggablePosition", JSON.stringify(position));
};

const onMouseMove = (event: MouseEvent) => {
if (isDragging) {
let newX = event.clientX - offset.x;
let newY = event.clientY - offset.y;

// Boundary checks
if (ref.current) {
const maxX = window.innerWidth - ref.current.offsetWidth;
const maxY = window.innerHeight - ref.current.offsetHeight;

if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
if (newX > maxX) newX = maxX;
if (newY > maxY) newY = maxY;

setPosition({ x: newX, y: newY });
}
}
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, [isDragging, offset.x, offset.y, position]);

return (
<div
ref={ref}
data-testid={"draggable"}
onMouseDown={onMouseDown}
className="absolute z-50 cursor-grab select-none bg-gray-300 rounded flex flex-col justify-center items-center"
style={{ left: position.x, top: position.y }}
>
<div className="bg-gray-500 text-white text-sm font-bold p-1 rounded-t w-full flex justify-between items-center">
<span>Debugger (Drag Me)</span>
<span className="text-2xl"></span>
</div>
<div className="p-2">{children}</div>
</div>
);
};

export default Draggable;